import { Vector3, Disposable, objectKeys } from '@zephyr3d/base';
import { QUEUE_TRANSPARENT } from '../values.js';
import { ProgramBuilder } from '@zephyr3d/device';
import { ShaderHelper } from '../material/shader/helper.js';
import '../material/lambert.js';
import '../material/blinn.js';
import '../material/unlit.js';
import '../material/particle.js';
import { RenderBundleWrapper } from './renderbundle_wrapper.js';
import { getDevice } from '../app/api.js';
import '../material/meshmaterial.js';
import '../material/grassmaterial.js';
import '../material/terrain-cm.js';
import '../material/pbrmr.js';
import '../material/pbrsg.js';
import '../material/pbrblueprint.js';
import '../material/sprite.js';
import '../material/sprite_std.js';
import '../utility/blueprint/material/texture.js';

const maxBufferSizeInFloats = 65536 / 4;
/**
 * Instance bind group allocator
 * @public
 */ class InstanceBindGroupAllocator {
    static _instanceBindGroupLayout = null;
    _bindGroupList = [];
    _allocFrameStamp;
    constructor(){
        this._allocFrameStamp = -1;
        this._bindGroupList = [];
    }
    allocateInstanceBindGroup(framestamp, sizeInFloats) {
        // Reset if render frame changed
        if (this._allocFrameStamp !== framestamp) {
            this._allocFrameStamp = framestamp;
            for (const k of this._bindGroupList){
                k.offset = 0;
            }
        }
        for (const k of this._bindGroupList){
            if (k.offset + sizeInFloats <= maxBufferSizeInFloats) {
                k.dirty = true;
                return k;
            }
        }
        if (!InstanceBindGroupAllocator._instanceBindGroupLayout) {
            const buildInfo = new ProgramBuilder(getDevice()).buildRender({
                vertex (pb) {
                    this[ShaderHelper.getInstanceDataUniformName()] = pb.vec4[65536 >> 4]().uniformBuffer(3);
                    pb.main(function() {});
                },
                fragment (pb) {
                    pb.main(function() {});
                }
            });
            InstanceBindGroupAllocator._instanceBindGroupLayout = buildInfo[2][3];
        }
        const bindGroup = {
            bindGroup: getDevice().createBindGroup(InstanceBindGroupAllocator._instanceBindGroupLayout),
            buffer: new Float32Array(maxBufferSizeInFloats),
            offset: 0,
            dirty: true
        };
        this._bindGroupList.push(bindGroup);
        return bindGroup;
    }
}
const defaultInstanceBindGroupAlloator = new InstanceBindGroupAllocator();
const tmpDrawablePos = new Vector3();
/**
 * A queue that contains the items to be rendered
 * @public
 */ class RenderQueue extends Disposable {
    /** @internal */ _itemList;
    /** @internal */ _renderPass;
    /** @internal */ _shadowedLightList;
    /** @internal */ _unshadowedLightList;
    /** @internal */ _sunLight;
    /** @internal */ _bindGroupAllocator;
    /** @internal */ _ref;
    /** @internal */ _instanceInfo;
    /** @internal */ _needSceneColor;
    _needSceneDepth;
    _needSceneColorWithDepth;
    /** @internal */ _drawTransparent;
    /** @internal */ _objectColorMaps;
    /**
   * Creates an instance of a render queue
   * @param renderPass - The render pass to which the render queue belongs
   */ constructor(renderPass, bindGroupAllocator){
        super();
        this._bindGroupAllocator = bindGroupAllocator ?? defaultInstanceBindGroupAlloator;
        this._itemList = null;
        this._renderPass = renderPass;
        this._shadowedLightList = [];
        this._unshadowedLightList = [];
        this._sunLight = null;
        this._ref = {
            ref: this
        };
        this._instanceInfo = new Map();
        this._needSceneColor = false;
        this._needSceneDepth = false;
        this._needSceneColorWithDepth = false;
        this._drawTransparent = false;
        this._objectColorMaps = [
            new Map()
        ];
    }
    /** The sun light */ get sunLight() {
        return this._sunLight;
    }
    set sunLight(light) {
        this._sunLight = light;
    }
    /** @internal */ needSceneColor() {
        return this._needSceneColor;
    }
    /** @internal */ needSceneDepth() {
        return this._needSceneDepth;
    }
    /** @internal */ needSceneColorWithDepth() {
        return this._needSceneColorWithDepth;
    }
    /** Whether this render queue has transparent objects to be drawn */ get drawTransparent() {
        return this._drawTransparent;
    }
    /** The render pass to which the render queue belongs */ get renderPass() {
        return this._renderPass;
    }
    /**
   * Gets the items of the render queue
   */ get itemList() {
        return this._itemList;
    }
    /**
   * Gets the shadowed lights
   */ get shadowedLights() {
        return this._shadowedLightList;
    }
    /**
   * Gets the unshadowed lights
   */ get unshadowedLights() {
        return this._unshadowedLightList;
    }
    /**
   * Gets the indirect reference of this
   */ get ref() {
        return this._ref;
    }
    /**
   * Gets the instance information for given drawable object
   * @param drawable - The drawable object
   * @returns The instane information for given drawable object, null if no exists
   */ getInstanceInfo(drawable) {
        return this._instanceInfo.get(drawable);
    }
    /**
   * Gets the maximum batch size of a given device
   * @returns The maximum batch size of the device
   *
   * @internal
   */ getMaxBatchSize() {
        return getDevice().getDeviceCaps().shaderCaps.maxUniformBufferSize / 64;
    }
    /**
   * Push a punctual light
   * @param light - The light to be pushed
   */ pushLight(light) {
        if (light.castShadow) {
            this._shadowedLightList.push(light);
        } else {
            this._unshadowedLightList.push(light);
        }
        if (light.isDirectionLight() && light.sunLight) {
            this.sunLight = light;
        }
    }
    /**
   * Push items from another render queue
   * @param queue - The render queue to be pushed
   */ pushRenderQueue(queue) {
        const newItemLists = queue._itemList;
        if (!newItemLists) {
            return;
        }
        if (!this._itemList) {
            this._itemList = this.newRenderItemList();
        }
        this._itemList.opaque.lit.push(...newItemLists.opaque.lit);
        this._itemList.opaque.unlit.push(...newItemLists.opaque.unlit);
        this._itemList.transmission.lit.push(...newItemLists.transmission.lit);
        this._itemList.transmission.unlit.push(...newItemLists.transmission.unlit);
        this._itemList.transparent.lit.push(...newItemLists.transparent.lit);
        this._itemList.transparent.unlit.push(...newItemLists.transparent.unlit);
        this._itemList.transmission_trans.lit.push(...newItemLists.transmission_trans.lit);
        this._itemList.transmission_trans.unlit.push(...newItemLists.transmission_trans.unlit);
        this._needSceneColor ||= queue._needSceneColor;
        this._needSceneDepth ||= queue._needSceneDepth;
        this._needSceneColorWithDepth ||= queue._needSceneColorWithDepth;
        this._drawTransparent ||= queue._drawTransparent;
        this._objectColorMaps.push(...queue._objectColorMaps);
    }
    /**
   * Push an item to the render queue
   * @param camera - The camera for drawing the item
   * @param drawable - The object to be drawn
   */ push(camera, drawable) {
        if (drawable) {
            drawable.pushRenderQueueRef(this._ref);
            if (!this._itemList) {
                this._itemList = this.newRenderItemList();
            }
            const trans = drawable.getQueueType() === QUEUE_TRANSPARENT;
            const unlit = drawable.isUnlit();
            const needDepth = !!drawable.needSceneDepth();
            const transmission = !!drawable.needSceneColor() || needDepth;
            this._needSceneColor ||= transmission;
            this._needSceneDepth ||= drawable.needSceneDepth();
            this._needSceneColorWithDepth ||= transmission && needDepth;
            this._drawTransparent ||= trans;
            if (camera.getPickResultResolveFunc()) {
                drawable.getMaterial().objectColor = drawable.getObjectColor();
                this._objectColorMaps[0].set(drawable.getDrawableId(), drawable);
            }
            if (drawable.isBatchable()) {
                const instanceList = trans ? transmission ? unlit ? this._itemList.transmission_trans.unlit[0].instanceList : this._itemList.transmission_trans.lit[0].instanceList : unlit ? this._itemList.transparent.unlit[0].instanceList : this._itemList.transparent.lit[0].instanceList : transmission ? unlit ? this._itemList.transmission.unlit[0].instanceList : this._itemList.transmission.lit[0].instanceList : unlit ? this._itemList.opaque.unlit[0].instanceList : this._itemList.opaque.lit[0].instanceList;
                const hash = drawable.getInstanceId(this._renderPass);
                let drawableList = instanceList[hash];
                if (!drawableList) {
                    drawableList = [];
                    instanceList[hash] = drawableList;
                }
                drawableList.push(drawable);
            } else {
                const list = trans ? transmission ? unlit ? this._itemList.transmission_trans.unlit[0] : this._itemList.transmission_trans.lit[0] : unlit ? this._itemList.transparent.unlit[0] : this._itemList.transparent.lit[0] : transmission ? unlit ? this._itemList.transmission.unlit[0] : this._itemList.transmission.lit[0] : unlit ? this._itemList.opaque.unlit[0] : this._itemList.opaque.lit[0];
                const skinAnimation = !!drawable.getBoneMatrices();
                const morphAnimation = !!drawable.getMorphData();
                let queue;
                if (skinAnimation && morphAnimation) {
                    queue = list.skinAndMorphItemList;
                } else if (skinAnimation) {
                    queue = list.skinItemList;
                } else if (morphAnimation) {
                    queue = list.morphItemList;
                } else {
                    queue = list.itemList;
                }
                this.binaryInsert(queue, {
                    drawable,
                    sortDistance: drawable.getSortDistance(camera),
                    instanceData: null
                });
                const mat = drawable.getMaterial();
                if (mat) {
                    list.materialList.add(mat.coreMaterial);
                }
                drawable.applyTransformUniforms(this);
            }
        }
    }
    /** @internal */ getDrawableByColor(c) {
        const id = (c[0] << 24) + (c[1] << 16) + (c[2] << 8) + c[3];
        for (const m of this._objectColorMaps){
            const drawable = m.get(id);
            if (drawable) {
                return drawable;
            }
        }
        return null;
    }
    /**
   * Removes all items in the render queue
   */ reset() {
        if (this._itemList) {
            // Release all render bundles
            for (const k of objectKeys(this._itemList)){
                const itemListBundle = this._itemList[k];
                for (const l of objectKeys(itemListBundle)){
                    const listInfo = itemListBundle[l];
                    for (const info of listInfo){
                        if (info.renderQueue === this) {
                            if (info.renderBundle) {
                                info.renderBundle.dispose();
                            }
                            if (info.skinRenderBundle) {
                                info.skinRenderBundle.dispose();
                            }
                            if (info.morphRenderBundle) {
                                info.morphRenderBundle.dispose();
                            }
                            if (info.skinAndMorphRenderBundle) {
                                info.skinAndMorphRenderBundle.dispose();
                            }
                            if (info.instanceRenderBundle) {
                                info.instanceRenderBundle.dispose();
                            }
                        }
                    }
                }
            }
            this._itemList = null;
        }
        this._shadowedLightList = [];
        this._unshadowedLightList = [];
        this._sunLight = null;
        this._needSceneColor = false;
        this._needSceneDepth = false;
        this._needSceneColorWithDepth = false;
        this._drawTransparent = false;
    }
    /** @internal */ end(camera, createRenderBundles) {
        const frameCounter = getDevice().frameInfo.frameCounter;
        const itemList = this._itemList;
        if (!this.itemList) {
            return this;
        }
        const lists = [
            itemList.opaque.lit,
            itemList.opaque.unlit,
            itemList.transmission.lit,
            itemList.transmission.unlit,
            itemList.transparent.lit,
            itemList.transparent.unlit,
            itemList.transmission_trans.lit,
            itemList.transmission_trans.unlit
        ];
        for(let i = 0; i < lists.length; i++){
            const list = lists[i];
            for (const info of list){
                if (info.renderQueue !== this) {
                    continue;
                }
                const instanceList = info.instanceList;
                for(const x in instanceList){
                    const drawables = instanceList[x];
                    let bindGroup = null;
                    let item = null;
                    for(let i = 0; i < drawables.length; i++){
                        const drawable = drawables[i];
                        const instanceUniforms = drawable.getInstanceUniforms();
                        const instanceUniformsSize = instanceUniforms?.length ?? 0;
                        const stride = ShaderHelper.MATERIAL_INSTANCE_DATA_OFFSET * 4 + instanceUniformsSize;
                        if (!bindGroup || bindGroup.offset + stride > maxBufferSizeInFloats) {
                            bindGroup = this._bindGroupAllocator.allocateInstanceBindGroup(frameCounter, stride);
                            item = {
                                drawable,
                                sortDistance: drawable.getSortDistance(camera),
                                instanceData: {
                                    bindGroup,
                                    offset: bindGroup.offset,
                                    numInstances: 0,
                                    stride
                                }
                            };
                            this.binaryInsert(info.instanceItemList, item);
                            drawable.applyInstanceOffsetAndStride(this, stride, bindGroup.offset);
                        }
                        const instanceInfo = {
                            bindGroup,
                            offset: bindGroup.offset
                        };
                        this._instanceInfo.set(drawable, instanceInfo);
                        drawable.applyTransformUniforms(this);
                        drawable.applyMaterialUniforms(instanceInfo);
                        bindGroup.offset += stride;
                        item.instanceData.numInstances++;
                        const mat = drawable.getMaterial();
                        if (mat) {
                            info.materialList.add(mat.coreMaterial);
                        }
                    }
                }
                info.instanceList = {};
                if (createRenderBundles) {
                    if (info.itemList.length > 0) {
                        info.renderBundle = new RenderBundleWrapper();
                    }
                    if (info.skinItemList.length > 0) {
                        info.skinRenderBundle = new RenderBundleWrapper();
                    }
                    if (info.morphItemList.length > 0) {
                        info.morphRenderBundle = new RenderBundleWrapper();
                    }
                    if (info.skinAndMorphItemList.length > 0) {
                        info.skinAndMorphRenderBundle = new RenderBundleWrapper();
                    }
                    if (info.instanceItemList.length > 0) {
                        info.instanceRenderBundle = new RenderBundleWrapper();
                    }
                }
            }
        }
        return this;
    }
    binaryInsert(itemList, item) {
        let left = 0;
        let right = itemList.length - 1;
        const newInstanceId = item.drawable.getMaterial().instanceId;
        while(left <= right){
            const mid = Math.floor((left + right) / 2);
            const instanceId = itemList[mid].drawable.getMaterial().instanceId;
            if (instanceId === newInstanceId) {
                itemList.splice(mid + 1, 0, item);
                return;
            } else if (instanceId < newInstanceId) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        itemList.splice(left, 0, item);
    }
    /**
   * Sorts the items in the render queue for rendering
   */ sortTransparentItems(cameraPos) {
        if (this._itemList) {
            this._itemList.transparent.lit[0].itemList.sort((a, b)=>this.drawableDistanceToCamera(b.drawable, cameraPos) - this.drawableDistanceToCamera(a.drawable, cameraPos));
            this._itemList.transparent.lit[0].skinItemList.sort((a, b)=>this.drawableDistanceToCamera(b.drawable, cameraPos) - this.drawableDistanceToCamera(a.drawable, cameraPos));
            this._itemList.transparent.lit[0].morphItemList.sort((a, b)=>this.drawableDistanceToCamera(b.drawable, cameraPos) - this.drawableDistanceToCamera(a.drawable, cameraPos));
            this._itemList.transparent.lit[0].skinAndMorphItemList.sort((a, b)=>this.drawableDistanceToCamera(b.drawable, cameraPos) - this.drawableDistanceToCamera(a.drawable, cameraPos));
            this._itemList.transparent.unlit[0].itemList.sort((a, b)=>this.drawableDistanceToCamera(b.drawable, cameraPos) - this.drawableDistanceToCamera(a.drawable, cameraPos));
            this._itemList.transparent.unlit[0].skinItemList.sort((a, b)=>this.drawableDistanceToCamera(b.drawable, cameraPos) - this.drawableDistanceToCamera(a.drawable, cameraPos));
            this._itemList.transparent.unlit[0].morphItemList.sort((a, b)=>this.drawableDistanceToCamera(b.drawable, cameraPos) - this.drawableDistanceToCamera(a.drawable, cameraPos));
            this._itemList.transparent.unlit[0].skinAndMorphItemList.sort((a, b)=>this.drawableDistanceToCamera(b.drawable, cameraPos) - this.drawableDistanceToCamera(a.drawable, cameraPos));
        }
    }
    /** Disposes the render queue */ onDispose() {
        super.onDispose();
        this.reset();
    }
    drawableDistanceToCamera(drawable, cameraPos) {
        const drawablePos = drawable.getNode().getWorldPosition(tmpDrawablePos);
        return Vector3.distanceSq(drawablePos, cameraPos);
    }
    newRenderItemListInfo() {
        return {
            itemList: [],
            skinItemList: [],
            morphItemList: [],
            skinAndMorphItemList: [],
            instanceItemList: [],
            materialList: new Set(),
            instanceList: {},
            renderQueue: this
        };
    }
    newRenderItemListBundle() {
        return {
            lit: [
                this.newRenderItemListInfo()
            ],
            unlit: [
                this.newRenderItemListInfo()
            ]
        };
    }
    newRenderItemList() {
        return {
            opaque: this.newRenderItemListBundle(),
            transmission: this.newRenderItemListBundle(),
            transparent: this.newRenderItemListBundle(),
            transmission_trans: this.newRenderItemListBundle()
        };
    }
}

export { InstanceBindGroupAllocator, RenderQueue };
//# sourceMappingURL=render_queue.js.map
