import { Mat4 } from '../../core/math/mat4.js';
import { Vec3 } from '../../core/math/vec3.js';
import { PIXELFORMAT_R32U, SEMANTIC_ATTR13, SEMANTIC_POSITION, CULLFACE_NONE } from '../../platform/graphics/constants.js';
import { MeshInstance } from '../mesh-instance.js';
import { GSplatResolveSH } from './gsplat-resolve-sh.js';
import { GSplatSorter } from './gsplat-sorter.js';
import { GSplatSogsData } from './gsplat-sogs-data.js';
import { GSplatResourceBase } from './gsplat-resource-base.js';
import { ShaderMaterial } from '../materials/shader-material.js';
import { BLEND_NONE, BLEND_PREMULTIPLIED } from '../constants.js';

/**
 * @import { Camera } from '../camera.js'
 * @import { GraphNode } from '../graph-node.js'
 * @import { Texture } from '../../platform/graphics/texture.js'
 */ const mat = new Mat4();
const cameraPosition = new Vec3();
const cameraDirection = new Vec3();
const viewport = [
    0,
    0
];
/** @ignore */ class GSplatInstance {
    /**
     * @param {GSplatResourceBase} resource - The splat instance.
     * @param {object} [options] - Options for the instance.
     * @param {ShaderMaterial|null} [options.material] - The material instance.
     * @param {boolean} [options.highQualitySH] - Whether to use the high quality or the approximate spherical harmonic calculation. Only applies to SOGS data.
     */ constructor(resource, options = {}){
        this.options = {};
        /** @type {GSplatSorter|null} */ this.sorter = null;
        this.lastCameraPosition = new Vec3();
        this.lastCameraDirection = new Vec3();
        /** @type {GSplatResolveSH|null} */ this.resolveSH = null;
        /**
     * List of cameras this instance is visible for. Updated every frame by the renderer.
     *
     * @type {Camera[]}
     * @ignore
     */ this.cameras = [];
        this.resource = resource;
        // create the order texture
        this.orderTexture = resource.createTexture('splatOrder', PIXELFORMAT_R32U, resource.evalTextureSize(resource.numSplats));
        if (options.material) {
            // material is provided
            this._material = options.material;
            // patch splat order
            this._material.setParameter('splatOrder', this.orderTexture);
        } else {
            // construct the material
            this._material = new ShaderMaterial({
                uniqueName: 'SplatMaterial',
                vertexGLSL: '#include "gsplatVS"',
                fragmentGLSL: '#include "gsplatPS"',
                vertexWGSL: '#include "gsplatVS"',
                fragmentWGSL: '#include "gsplatPS"',
                attributes: {
                    vertex_position: SEMANTIC_POSITION,
                    vertex_id_attrib: SEMANTIC_ATTR13
                }
            });
            // default configure
            this.configureMaterial(this._material);
            // update
            this._material.update();
        }
        this.meshInstance = new MeshInstance(resource.mesh, this._material);
        this.meshInstance.setInstancing(resource.instanceIndices, true);
        this.meshInstance.gsplatInstance = this;
        // only start rendering the splat after we've received the splat order data
        this.meshInstance.instancingCount = 0;
        // clone centers to allow multiple instances of sorter
        const centers = resource.centers.slice();
        const chunks = resource.chunks?.slice();
        // create sorter
        this.sorter = new GSplatSorter();
        this.sorter.init(this.orderTexture, centers, chunks);
        this.sorter.on('updated', (count)=>{
            // limit splat render count to exclude those behind the camera
            this.meshInstance.instancingCount = Math.ceil(count / GSplatResourceBase.instanceSize);
            // update splat count on the material
            this.material.setParameter('numSplats', count);
        });
        // configure sogs sh resolve
        this.setHighQualitySH(options.highQualitySH ?? false);
    }
    destroy() {
        this.resolveSH?.destroy();
        this.material?.destroy();
        this.meshInstance?.destroy();
        this.sorter?.destroy();
    }
    /**
     * @param {ShaderMaterial} value - The material instance.
     */ set material(value) {
        if (this._material !== value) {
            // set the new material
            this._material = value;
            // patch order texture
            this._material.setParameter('splatOrder', this.orderTexture);
            if (this.meshInstance) {
                this.meshInstance.material = value;
            }
        }
    }
    get material() {
        return this._material;
    }
    /**
     * Configure the material with gsplat instance and resource properties.
     *
     * @param {ShaderMaterial} material - The material to configure.
     * @param {object} [options] - Object for passing optional arguments.
     * @param {boolean} [options.dither] - Specify true to configure the material for dithered rendering (stochastic alpha).
     */ configureMaterial(material, options = {}) {
        // allow resource to configure the material
        this.resource.configureMaterial(material);
        // set instance properties
        material.setParameter('numSplats', 0);
        material.setParameter('splatOrder', this.orderTexture);
        material.setParameter('alphaClip', 0.3);
        material.setDefine(`DITHER_${options.dither ? 'BLUENOISE' : 'NONE'}`, '');
        material.cull = CULLFACE_NONE;
        material.blendType = options.dither ? BLEND_NONE : BLEND_PREMULTIPLIED;
        material.depthWrite = !!options.dither;
    }
    updateViewport(cameraNode) {
        const camera = cameraNode?.camera;
        const renderTarget = camera?.renderTarget;
        const { width, height } = renderTarget ?? this.resource.device;
        viewport[0] = width;
        viewport[1] = height;
        // adjust viewport for stereoscopic VR sessions
        const xr = camera?.camera?.xr;
        if (xr?.active && xr.views.list.length === 2) {
            viewport[0] *= 0.5;
        }
        this.material.setParameter('viewport', viewport);
    }
    /**
     * Sorts the GS vertices based on the given camera.
     * @param {GraphNode} cameraNode - The camera node used for sorting.
     */ sort(cameraNode) {
        if (this.sorter) {
            const cameraMat = cameraNode.getWorldTransform();
            cameraMat.getTranslation(cameraPosition);
            cameraMat.getZ(cameraDirection);
            const modelMat = this.meshInstance.node.getWorldTransform();
            const invModelMat = mat.invert(modelMat);
            invModelMat.transformPoint(cameraPosition, cameraPosition);
            invModelMat.transformVector(cameraDirection, cameraDirection);
            // sort if the camera has changed
            if (!cameraPosition.equalsApprox(this.lastCameraPosition) || !cameraDirection.equalsApprox(this.lastCameraDirection)) {
                this.lastCameraPosition.copy(cameraPosition);
                this.lastCameraDirection.copy(cameraDirection);
                this.sorter.setCamera(cameraPosition, cameraDirection);
            }
        }
        this.updateViewport(cameraNode);
    }
    update() {
        if (this.cameras.length > 0) {
            // sort by the first camera it's visible for
            // TODO: extend to support multiple cameras
            const camera = this.cameras[0];
            this.sort(camera._node);
            // resolve spherical harmonics
            this.resolveSH?.render(camera._node, this.meshInstance.node.getWorldTransform());
            // we get new list of cameras each frame
            this.cameras.length = 0;
        }
    }
    setHighQualitySH(value) {
        const { resource } = this;
        const { gsplatData } = resource;
        if (gsplatData instanceof GSplatSogsData && gsplatData.shBands > 0 && value === !!this.resolveSH) {
            if (this.resolveSH) {
                this.resolveSH.destroy();
                this.resolveSH = null;
            } else {
                this.resolveSH = new GSplatResolveSH(resource.device, this);
            }
        }
    }
}

export { GSplatInstance };
