import { Disposable, DRef, AABB, ClipState, nextPowerOf2, DWeakRef } from '@zephyr3d/base';
import { getDevice } from '../../app/api.js';
import '../../values.js';
import '../../render/render_queue.js';
import { Primitive } from '../../render/primitive.js';
import '@zephyr3d/device';
import '../../material/shader/helper.js';
import '../../material/lambert.js';
import '../../material/blinn.js';
import '../../material/unlit.js';
import '../../material/particle.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';
import '../../render/rendergraph/forward_plus_builder.js';
import '../../render/sky.js';
import '../../render/clipmap.js';
import '../../shaders/atmosphere.js';
import { ClipmapGrassMaterial } from './grassmaterial.js';

const MAX_INSTANCES_PER_NODE = 2048;
// Two floats
const INSTANCE_BYTES = 4 * 4;
/**
 * Grass glade instance manager class
 * @internal
 */ class GrassInstances extends Disposable {
    _instances;
    _baseVertexBuffer;
    _indexBuffer;
    _instanceBuffer;
    _primitive;
    constructor(baseVertexBuffer, indexBuffer){
        super();
        this._instances = [];
        this._baseVertexBuffer = new DRef(baseVertexBuffer);
        this._indexBuffer = new DRef(indexBuffer);
        this._instanceBuffer = new DRef();
        this._primitive = new DRef();
    }
    get numInstances() {
        return this._instances.length;
    }
    get instanceBuffer() {
        return this._instanceBuffer.get();
    }
    setBaseVertexBuffer(baseVertexBuffer) {
        if (baseVertexBuffer !== this._baseVertexBuffer.get()) {
            this._baseVertexBuffer.set(baseVertexBuffer);
            this._primitive.dispose();
        }
    }
    draw() {
        if (this._instances.length > 0) {
            if (!this._primitive.get()) {
                const primitive = new Primitive();
                primitive.setVertexBuffer(this._baseVertexBuffer.get());
                primitive.setVertexBuffer(this._instanceBuffer.get(), 'instance');
                primitive.setIndexBuffer(this._indexBuffer.get());
                primitive.primitiveType = 'triangle-list';
                primitive.indexStart = 0;
                primitive.indexCount = this._indexBuffer.get().length;
                this._primitive.set(primitive);
            }
            this._primitive.get().drawInstanced(this._instances.length);
        }
    }
    updateBuffers() {
        const device = getDevice();
        if (this._instances.length === 0) {
            this._instanceBuffer.set(null);
            return;
        }
        if (this._instances.length > 0 && !this._instanceBuffer.get()) {
            this._instanceBuffer.set(device.createVertexBuffer('tex1_f32x4', new Uint8Array(this._instances.length * INSTANCE_BYTES)));
        }
        let buffer = this._instanceBuffer.get();
        const currentBytes = buffer.byteLength;
        const bytesRequired = this._instances.length * INSTANCE_BYTES;
        if (currentBytes < bytesRequired) {
            buffer = device.createVertexBuffer('tex1_f32x4', new Uint8Array(nextPowerOf2(bytesRequired)));
            this._instanceBuffer.set(buffer);
            this._primitive.dispose();
        }
        const data = new Float32Array(4 * this._instances.length);
        for(let i = 0; i < this._instances.length; i++){
            data[i * 4 + 0] = this._instances[i].x;
            data[i * 4 + 1] = this._instances[i].y;
            data[i * 4 + 2] = Math.sin(this._instances[i].angle);
            data[i * 4 + 3] = Math.cos(this._instances[i].angle);
        }
        buffer.bufferSubData(0, data);
    }
    removeInstances(minX, minZ, maxX, maxZ, num) {
        if (num <= 0) {
            return 0;
        }
        let removed = 0;
        for(let i = this._instances.length - 1; i >= 0; i--){
            const instance = this._instances[i];
            if (instance.x >= minX && instance.x <= maxX && instance.y >= minZ && instance.y <= maxZ) {
                this._instances.splice(i, 1);
                removed++;
                if (removed === num) {
                    break;
                }
            }
        }
        if (removed > 0) {
            this.updateBuffers();
        }
        return removed;
    }
    addInstances(instances) {
        this._instances.push(...instances);
        this.updateBuffers();
    }
    onDispose() {
        super.onDispose();
        this._baseVertexBuffer.dispose();
        this._indexBuffer.dispose();
        this._instanceBuffer.dispose();
        this._primitive.dispose();
    }
}
/**
 * Grass layer class
 * @public
 */ class GrassLayer extends Disposable {
    static _indexBuffer = new DRef();
    _material;
    _quadtree;
    _bladeWidth;
    _bladeHeight;
    _baseVertexBuffer;
    /**
   * Creates an instance of GrassLayer
   * @param terrain - Clipmap terrain object
   * @param bladeWidth - Grass blade width
   * @param bladeHeight - Grass blade height
   * @param albedoMap - Albedo texture for the blade
   */ constructor(terrain, bladeWidth, bladeHeight, albedoMap){
        super();
        this._material = new DRef(new ClipmapGrassMaterial(terrain));
        this._material.get().albedoTexture = albedoMap ?? null;
        if (albedoMap) {
            this._material.get().setTextureSize(albedoMap.width, albedoMap.height);
        }
        this._bladeWidth = bladeWidth;
        this._bladeHeight = bladeHeight;
        this._baseVertexBuffer = new DRef(this.createBaseVertexBuffer(this._bladeWidth, this._bladeHeight));
        this._quadtree = new DRef(new GrassQuadtreeNode(this._baseVertexBuffer.get(), GrassLayer._getIndexBuffer()));
    }
    /** @internal */ get quadtree() {
        return this._quadtree.get();
    }
    /** @internal */ updateMaterial() {
        this._material.get().uniformChanged();
    }
    /**
   * Sets the albedo texture of grass blades in this layer
   * @param albedoMap - Albedo texture to set
   */ setAlbedoMap(albedoMap) {
        this._material.get().albedoTexture = albedoMap;
        if (albedoMap) {
            this._material.get().setTextureSize(albedoMap.width, albedoMap.height);
        }
    }
    /**
   * Gets the albedo texture of grass blades in this layer
   * @returns - Albedo texture of grass blades in this layer
   */ getAlbedoMap() {
        return this._material.get().albedoTexture;
    }
    /**
   * Add grass blades to this layer in a region
   * @param instances - Grass blade instances to add
   */ addInstances(instances) {
        return this._quadtree.get().addInstances(instances);
    }
    /**
   * Remove grass blades to this layer in a region
   * @param minX - Minimum x position of the region
   * @param minZ - Minimum z position of the region
   * @param maxX - Maximum x position of the region
   * @param maxZ - Maximum z position of the region
   * @param numInstances - How many grass blades to add
   */ removeInstances(minX, minZ, maxX, maxZ, numInstances) {
        this._quadtree.get().removeInstances(minX, minZ, maxX, maxZ, numInstances);
    }
    /** Grass blade width in this layer */ get bladeWidth() {
        return this._bladeWidth;
    }
    set bladeWidth(val) {
        this.setBladeSize(val, this._bladeHeight);
    }
    /** Grass blade height in this layer */ get bladeHeight() {
        return this._bladeHeight;
    }
    set bladeHeight(val) {
        this.setBladeSize(this._bladeWidth, val);
    }
    /**
   * Sets the size of grass blades in this layer
   * @param width - Grass blade width
   * @param height - Grass blade height
   */ setBladeSize(width, height) {
        if (width !== this._bladeWidth || height !== this._bladeHeight) {
            this._bladeWidth = width;
            this._bladeHeight = height;
            this._baseVertexBuffer.set(this.createBaseVertexBuffer(this._bladeWidth, this._bladeHeight));
            this._quadtree.get().setBaseVertexBuffer(this._baseVertexBuffer.get());
        }
    }
    /** @internal */ static _getIndexBuffer() {
        if (!this._indexBuffer.get()) {
            this._indexBuffer.set(getDevice().createIndexBuffer(new Uint16Array([
                0,
                1,
                2,
                0,
                2,
                3,
                4,
                5,
                6,
                4,
                6,
                7,
                8,
                9,
                10,
                8,
                10,
                11
            ])));
        }
        return this._indexBuffer.get();
    }
    /** @internal */ createBaseVertexBuffer(bladeWidth, bladeHeight) {
        const device = getDevice();
        const r = bladeWidth * 0.5;
        const t = bladeHeight;
        const c = r * Math.cos(Math.PI / 3);
        const s = r * Math.sin(Math.PI / 3);
        const vertices = new Float32Array([
            r,
            0,
            0,
            0,
            1,
            r,
            t,
            0,
            0,
            0,
            -r,
            t,
            0,
            1,
            0,
            -r,
            0,
            0,
            1,
            1,
            c,
            0,
            s,
            0,
            1,
            -c,
            0,
            -s,
            1,
            1,
            -c,
            t,
            -s,
            1,
            0,
            c,
            t,
            s,
            0,
            0,
            -c,
            0,
            s,
            0,
            1,
            c,
            0,
            -s,
            1,
            1,
            c,
            t,
            -s,
            1,
            0,
            -c,
            t,
            s,
            0,
            0
        ]);
        return device.createInterleavedVertexBuffer([
            'position_f32x3',
            'tex0_f32x2'
        ], vertices);
    }
    /** @internal */ draw(ctx, region, minY, maxY) {
        this._material.get().apply(ctx);
        for(let pass = 0; pass < this._material.get().numPasses; pass++){
            this._material.get().bind(ctx.device, pass);
            this._quadtree.get().draw(ctx.camera, region, minY, maxY, false);
        }
    }
    /** @internal */ onDispose() {
        super.onDispose();
        this._material.dispose();
        this._quadtree.dispose();
        this._baseVertexBuffer.dispose();
    }
}
/**
 * Grass renderer for clipmap terrain
 * @public
 */ class GrassRenderer extends Disposable {
    _terrain;
    _numGrassBlades;
    _layers;
    /**
   * Creates an instance of GrassRenderer
   * @param terrain - Clipmap terrain object
   */ constructor(terrain){
        super();
        this._terrain = new DWeakRef(terrain);
        this._numGrassBlades = 0;
        this._layers = [];
    }
    /** @internal */ updateMaterial() {
        for (const layer of this._layers){
            layer.updateMaterial();
        }
    }
    /** How many grass blades */ get numGrassBlades() {
        return this._numGrassBlades;
    }
    /** How many grass layers */ get numLayers() {
        return this._layers.length;
    }
    /**
   * Gets the grass layer at given index
   * @param index - Index of the grass layer
   * @returns The grass layer at the index
   */ getLayer(index) {
        return this._layers[index];
    }
    /**
   * Adds a grass layer
   * @param bladeWidth - Width of grass blades in this layer
   * @param bladeHeight - Height of grass blades in this layer
   * @param albedoMap - Albedo texture of grass blades in this layer
   * @returns Index of the added grass layer
   */ addLayer(bladeWidth, bladeHeight, albedoMap) {
        const layer = new GrassLayer(this._terrain.get(), bladeWidth, bladeHeight, albedoMap);
        this._layers.push(layer);
        return this._layers.length - 1;
    }
    /**
   * Gets the albedo texture of grass blades in the grass layer at given index
   * @param layer - Index of the grass layer to get
   * @returns Albedo texture of grass blades in the grass layer
   */ getGrassTexture(layer) {
        return this._layers[layer]?.getAlbedoMap() ?? null;
    }
    /**
   * Sets the albedo texture of grass blades in the grass layer at given index
   * @param layer - Index of the grass layer to set
   * @param texture - Albedo texture to set
   */ setGrassTexture(layer, texture) {
        const grassLayer = this._layers[layer];
        if (grassLayer) {
            grassLayer.setAlbedoMap(texture);
        } else {
            console.error(`Invalid grass layer: ${layer}`);
        }
    }
    /**
   * Gets width of the grass blades in the grass layer at given index
   * @param layer - Index of the grass layer
   * @returns Width of the grass blades in the layer
   */ getBladeWidth(layer) {
        return this._layers[layer]?.bladeWidth ?? 0;
    }
    /**
   * Gets height of the grass blades in the grass layer at given index
   * @param layer - Index of the grass layer
   * @returns Height of the grass blades in the layer
   */ getBladeHeight(layer) {
        return this._layers[layer]?.bladeHeight ?? 0;
    }
    /**
   * Sets size of the grass blades in the grass layer at given index
   * @param layer - Index of the grass layer
   * @param width - Width to set
   * @param height - Height to set
   */ setBladeSize(layer, width, height) {
        const grassLayer = this._layers[layer];
        if (grassLayer) {
            grassLayer.setBladeSize(width, height);
        } else {
            console.error(`Invalid grass layer: ${layer}`);
        }
    }
    /**
   * Add grass instances to the grass layer at given index
   * @param layer - Index of the grass layer
   * @param instances - Grass blade instances to add
   */ addInstances(layer, instances) {
        const grassLayer = this._layers[layer];
        if (!grassLayer) {
            console.error(`Invalid grass layer: ${layer}`);
        } else {
            this._numGrassBlades += grassLayer.addInstances(instances);
        }
    }
    /**
   * Remove grass instances from the grass layer at given index within a region
   * @param layer - Index of the grass layer
   * @param minX - Minimum x of the region
   * @param minZ - Minimum z of the region
   * @param maxX - Maximum x of the region
   * @param maxZ - Maximum z of the region
   * @param num - How many grass blades to remove
   */ removeInstances(layer, minX, minZ, maxX, maxZ, num) {
        const grassLayer = this._layers[layer];
        if (!grassLayer) {
            console.error(`Invalid grass layer: ${layer}`);
        } else {
            grassLayer.removeInstances(minX, minZ, maxX, maxZ, num);
        }
    }
    /** @internal */ draw(ctx) {
        const bv = this._terrain.get().getWorldBoundingVolume().toAABB();
        const minY = bv.minPoint.y;
        const maxY = bv.maxPoint.y;
        for (const layer of this._layers){
            layer.draw(ctx, this._terrain.get().worldRegion, minY - layer.bladeHeight, maxY + layer.bladeHeight);
        }
    }
    onDispose() {
        super.onDispose();
        this._terrain.dispose();
        for (const layer of this._layers){
            layer.dispose();
        }
    }
}
/** @internal */ class GrassQuadtreeNode extends Disposable {
    static _cullAABB = new AABB();
    _grassInstances;
    _children;
    _baseVertexBuffer;
    _indexBuffer;
    _minX;
    _minZ;
    _maxX;
    _maxZ;
    constructor(baseVertexBuffer, indexBuffer){
        super();
        this._baseVertexBuffer = new DRef(baseVertexBuffer);
        this._indexBuffer = new DRef(indexBuffer);
        this._grassInstances = new DRef(new GrassInstances(baseVertexBuffer, indexBuffer));
        this._children = null;
        this._minX = 0;
        this._minZ = 0;
        this._maxX = 1;
        this._maxZ = 1;
    }
    get grassInstances() {
        return this._grassInstances.get();
    }
    get children() {
        return this._children;
    }
    draw(camera, region, minY, maxY, skipClipTest) {
        if (!skipClipTest) {
            const cullAABB = GrassQuadtreeNode._cullAABB;
            const x = region.x;
            const z = region.y;
            const dx = region.z - x;
            const dz = region.w - z;
            cullAABB.minPoint.setXYZ(x + this._minX * dx, minY, z + this._minZ * dz);
            cullAABB.maxPoint.setXYZ(x + this._maxX * dx, maxY, z + this._maxZ * dz);
            const clipState = camera.clipMask ? cullAABB.getClipStateWithFrustumMask(camera.frustum, camera.clipMask) : cullAABB.getClipStateWithFrustum(camera.frustum);
            if (clipState === ClipState.NOT_CLIPPED) {
                return;
            }
            skipClipTest = clipState === ClipState.A_INSIDE_B;
        }
        if (this._grassInstances.get().numInstances > 0) {
            this._grassInstances.get().draw();
        }
        if (this._children) {
            for (const child of this._children){
                child.draw(camera, region, minY, maxY, skipClipTest);
            }
        }
    }
    setBaseVertexBuffer(baseVertexBuffer) {
        if (baseVertexBuffer !== this._baseVertexBuffer.get()) {
            this._baseVertexBuffer.set(baseVertexBuffer);
            this._grassInstances.get().setBaseVertexBuffer(baseVertexBuffer);
            if (this._children) {
                for (const child of this._children){
                    child.setBaseVertexBuffer(baseVertexBuffer);
                }
            }
        }
    }
    removeInstances(minX, minZ, maxX, maxZ, numInstances) {
        let n = Math.min(this._grassInstances.get().numInstances, numInstances);
        if (n <= 0) {
            return 0;
        }
        let removed = 0;
        if (this._children) {
            for (const child of this._children){
                if (child._minX < maxX && child._minZ < maxZ && child._maxX > minX && child._maxZ > minZ) {
                    removed += child.removeInstances(minX, minZ, maxX, maxZ, n);
                    n -= removed;
                }
            }
        }
        if (n > 0) {
            removed += this._grassInstances.get().removeInstances(minX, minZ, maxX, maxZ, n);
        }
        return removed;
    }
    addInstances(instances) {
        if (instances.length === 0) {
            return 0;
        }
        let n = Math.min(instances.length, MAX_INSTANCES_PER_NODE - this._grassInstances.get().numInstances);
        this._grassInstances.get().addInstances(instances.slice(0, n));
        if (n < instances.length) {
            if (!this._children) {
                this._children = [
                    new GrassQuadtreeNode(this._baseVertexBuffer.get(), this._indexBuffer.get()),
                    new GrassQuadtreeNode(this._baseVertexBuffer.get(), this._indexBuffer.get()),
                    new GrassQuadtreeNode(this._baseVertexBuffer.get(), this._indexBuffer.get()),
                    new GrassQuadtreeNode(this._baseVertexBuffer.get(), this._indexBuffer.get())
                ];
                this._children[0]._minX = this._minX;
                this._children[0]._minZ = this._minZ;
                this._children[0]._maxX = (this._minX + this._maxX) * 0.5;
                this._children[0]._maxZ = (this._minZ + this._maxZ) * 0.5;
                this._children[1]._minX = this._children[0]._maxX;
                this._children[1]._minZ = this._minZ;
                this._children[1]._maxX = this._maxX;
                this._children[1]._maxZ = this._children[0]._maxZ;
                this._children[2]._minX = this._minX;
                this._children[2]._minZ = this._children[0]._maxZ;
                this._children[2]._maxX = this._children[0]._maxX;
                this._children[2]._maxZ = this._maxZ;
                this._children[3]._minX = this._children[0]._maxX;
                this._children[3]._minZ = this._children[0]._maxZ;
                this._children[3]._maxX = this._maxX;
                this._children[3]._maxZ = this._maxZ;
            }
            instances = instances.slice(n);
            for (const child of this._children){
                if (instances.length > 0) {
                    const childInstances = instances.filter((val)=>val.x >= child._minX && val.y >= child._minZ && val.x < child._maxX && val.y < child._maxZ);
                    n += child.addInstances(childInstances);
                    instances = instances.filter((val)=>!(val.x >= child._minX && val.y >= child._minZ && val.x < child._maxX && val.y < child._maxZ));
                }
            }
        }
        return n;
    }
    onDispose() {
        super.onDispose();
        this._baseVertexBuffer.dispose();
        this._indexBuffer.dispose();
        this._grassInstances.dispose();
        if (this._children) {
            for (const child of this._children){
                child.dispose();
            }
        }
    }
}

export { GrassInstances, GrassLayer, GrassQuadtreeNode, GrassRenderer };
//# sourceMappingURL=grass.js.map
