/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module Rendering
 */
import { Point2d, Point3d, Range3d, Transform, Vector2d } from "@itwin/core-geometry";
import { ColorDef, ColorIndex, Feature, FeatureIndex, FeatureTable, FillFlags, PackedFeatureTable, QParams3d, QPoint3dList, } from "@itwin/core-common";
import { GraphicBranch } from "./GraphicBranch";
import { DisplayParams } from "../common/internal/render/DisplayParams";
import { createMeshParams } from "../common/internal/render/VertexTableBuilder";
import { IModelApp } from "../IModelApp";
/**
 * @public
 * @extensions
 */
export var ParticleCollectionBuilder;
(function (ParticleCollectionBuilder) {
    /** Creates a new ParticleCollectionBuilder.
     * @throws Error if size is not greater than zero.
     */
    function create(params) {
        return new Builder(params);
    }
    ParticleCollectionBuilder.create = create;
})(ParticleCollectionBuilder || (ParticleCollectionBuilder = {}));
class Particle {
    constructor(centroid, width, height, transparency, rotationMatrix) {
        this.centroid = Point3d.fromJSON(centroid);
        this.transparency = transparency;
        this.width = width;
        this.height = height;
        this.rotationMatrix = rotationMatrix;
    }
}
class Builder {
    constructor(params) {
        this._hasVaryingTransparency = false;
        this._range = Range3d.createNull();
        this._particlesOpaque = [];
        this._particlesTranslucent = [];
        this._viewport = params.viewport;
        this._isViewCoords = true === params.isViewCoords;
        this._pickableId = params.pickableId;
        this._texture = params.texture;
        this._transparency = undefined !== params.transparency ? clampTransparency(params.transparency) : 0;
        this._localToWorldTransform = params.origin ? Transform.createTranslationXYZ(params.origin.x, params.origin.y, params.origin.z) : Transform.createIdentity();
        if ("number" === typeof params.size)
            this._size = new Vector2d(params.size, params.size);
        else
            this._size = Vector2d.fromJSON(params.size);
        if (this._size.x <= 0 || this._size.y <= 0)
            throw new Error("Particle size must be greater than zero");
    }
    get size() {
        return this._size;
    }
    get transparency() {
        return this._transparency;
    }
    set transparency(transparency) {
        transparency = clampTransparency(transparency);
        if (transparency !== this._transparency) {
            this._transparency = transparency;
            this._hasVaryingTransparency = this._particlesTranslucent.length > 0;
        }
    }
    addParticle(props) {
        const size = props.size ?? this._size;
        let width, height;
        if ("number" === typeof size) {
            width = height = size;
        }
        else {
            width = size.x;
            height = size.y;
        }
        if (width <= 0 || height <= 0)
            throw new Error("A particle must have a size greater than zero");
        const transparency = undefined !== props.transparency ? clampTransparency(props.transparency) : this.transparency;
        if (transparency !== this.transparency && this._particlesTranslucent.length > 0)
            this._hasVaryingTransparency = true;
        const particle = new Particle(props, width, height, transparency, props.rotationMatrix);
        if (transparency > 0)
            this._particlesTranslucent.push(particle);
        else
            this._particlesOpaque.push(particle);
        this._range.extendPoint(particle.centroid);
    }
    finish() {
        if (0 === this._particlesTranslucent.length + this._particlesOpaque.length)
            return undefined;
        // Order-independent transparency doesn't work well with opaque geometry - it will look semi-transparent.
        // If we have a mix of opaque and transparent particles, put them in separate graphics to be rendered in separate passes.
        const opaque = this.createGraphic(this._particlesOpaque, 0);
        const transparent = this.createGraphic(this._particlesTranslucent, this._hasVaryingTransparency ? undefined : this._transparency);
        // Empty the collection before any return statements.
        const range = this._range.clone();
        this._range.setNull();
        this._particlesOpaque.length = 0;
        this._particlesTranslucent.length = 0;
        this._hasVaryingTransparency = false;
        if (!transparent && !opaque)
            return undefined;
        // Transform from origin to collection, then to world.
        const toCollection = Transform.createTranslation(range.center);
        const toWorld = toCollection.multiplyTransformTransform(this._localToWorldTransform);
        const branch = new GraphicBranch(true);
        if (opaque)
            branch.add(opaque);
        if (transparent)
            branch.add(transparent);
        let graphic = this._viewport.target.renderSystem.createGraphicBranch(branch, toWorld);
        // If we have a pickable Id, produce a batch.
        // NB: We pass this._pickableId as the FeatureTable's modelId so that it will be treated like a reality model or a map -
        // specifically, it can be located and display a tooltip, but can't be selected.
        const featureTable = this._pickableId ? new FeatureTable(1, this._pickableId) : undefined;
        if (featureTable) {
            this._localToWorldTransform.multiplyRange(range, range);
            featureTable.insert(new Feature(this._pickableId));
            graphic = this._viewport.target.renderSystem.createBatch(graphic, PackedFeatureTable.pack(featureTable), range);
        }
        return graphic;
    }
    createGraphic(particles, uniformTransparency) {
        const numParticles = particles.length;
        if (numParticles <= 0)
            return undefined;
        // To keep scale values close to 1, compute mean size to use as size of quad.
        const meanSize = new Vector2d();
        let maxSize = 0;
        for (const particle of particles) {
            meanSize.x += particle.width;
            meanSize.y += particle.height;
            if (particle.width > maxSize)
                maxSize = particle.width;
            if (particle.height > maxSize)
                maxSize = particle.height;
        }
        meanSize.x /= numParticles;
        meanSize.y /= numParticles;
        // Define InstancedGraphicParams for particles.
        const rangeCenter = this._range.center;
        const floatsPerTransform = 12;
        const transforms = new Float32Array(floatsPerTransform * numParticles);
        const bytesPerOverride = 8;
        const symbologyOverrides = undefined === uniformTransparency ? new Uint8Array(bytesPerOverride * numParticles) : undefined;
        const viewToWorld = this._viewport.view.getRotation().transpose();
        let tfIndex = 0;
        let ovrIndex = 0;
        for (const particle of particles) {
            const scaleX = particle.width / meanSize.x;
            const scaleY = particle.height / meanSize.y;
            if (this._isViewCoords) {
                // Particles already face the camera in view coords - just apply the scale.
                transforms[tfIndex + 0] = scaleX;
                transforms[tfIndex + 5] = scaleY;
                transforms[tfIndex + 10] = 1;
            }
            else if (undefined !== particle.rotationMatrix) {
                // Scale rotation matrix relative to size of quad.
                transforms[tfIndex + 0] = particle.rotationMatrix.coffs[0] * scaleX;
                transforms[tfIndex + 1] = particle.rotationMatrix.coffs[1] * scaleY;
                transforms[tfIndex + 2] = particle.rotationMatrix.coffs[2];
                transforms[tfIndex + 4] = particle.rotationMatrix.coffs[3] * scaleX;
                transforms[tfIndex + 5] = particle.rotationMatrix.coffs[4] * scaleY;
                transforms[tfIndex + 6] = particle.rotationMatrix.coffs[5];
                transforms[tfIndex + 8] = particle.rotationMatrix.coffs[6] * scaleX;
                transforms[tfIndex + 9] = particle.rotationMatrix.coffs[7] * scaleY;
                transforms[tfIndex + 10] = particle.rotationMatrix.coffs[8];
            }
            else {
                // Rotate about origin by inverse view matrix so quads always face the camera and scale relative to size of quad.
                transforms[tfIndex + 0] = viewToWorld.coffs[0] * scaleX;
                transforms[tfIndex + 1] = viewToWorld.coffs[1] * scaleY;
                transforms[tfIndex + 2] = viewToWorld.coffs[2];
                transforms[tfIndex + 4] = viewToWorld.coffs[3] * scaleX;
                transforms[tfIndex + 5] = viewToWorld.coffs[4] * scaleY;
                transforms[tfIndex + 6] = viewToWorld.coffs[5];
                transforms[tfIndex + 8] = viewToWorld.coffs[6] * scaleX;
                transforms[tfIndex + 9] = viewToWorld.coffs[7] * scaleY;
                transforms[tfIndex + 10] = viewToWorld.coffs[8];
            }
            // Translate relative to center of particles range.
            transforms[tfIndex + 3] = particle.centroid.x - rangeCenter.x;
            transforms[tfIndex + 7] = particle.centroid.y - rangeCenter.y;
            transforms[tfIndex + 11] = particle.centroid.z - rangeCenter.z;
            tfIndex += floatsPerTransform;
            if (symbologyOverrides) {
                // See FeatureOverrides.buildLookupTable() for layout.
                symbologyOverrides[ovrIndex + 0] = 1 << 2; // OvrFlags.Alpha
                symbologyOverrides[ovrIndex + 7] = 0xff - particle.transparency;
                ovrIndex += bytesPerOverride;
            }
        }
        // Produce instanced quads.
        // Note: We do not need to allocate an array of featureIds. If we have a pickableId, all particles refer to the same Feature, with index 0.
        // So we leave the vertex attribute disabled causing the shader to receive the default (0, 0, 0) which happens to correspond to our feature index.
        const quad = createQuad(meanSize, this._texture, uniformTransparency ?? 0x7f, this._viewport);
        const transformCenter = new Point3d(0, 0, 0);
        const range = computeRange(this._range, rangeCenter, maxSize);
        const instances = { count: numParticles, transforms, transformCenter, symbologyOverrides, range };
        return this._viewport.target.renderSystem.createMesh(quad, instances);
    }
}
function createQuad(size, texture, transparency, viewport) {
    const halfWidth = size.x / 2;
    const halfHeight = size.y / 2;
    const corners = [
        new Point3d(-halfWidth, -halfHeight, 0), new Point3d(halfWidth, -halfHeight, 0),
        new Point3d(-halfWidth, halfHeight, 0), new Point3d(halfWidth, halfHeight, 0),
    ];
    const range = new Range3d();
    range.low = corners[0];
    range.high = corners[3];
    const points = new QPoint3dList(QParams3d.fromRange(range));
    for (const corner of corners)
        points.add(corner);
    const colors = new ColorIndex();
    colors.initUniform(ColorDef.white.withTransparency(transparency));
    const quadArgs = {
        points,
        vertIndices: [0, 1, 2, 2, 1, 3],
        fillFlags: FillFlags.None,
        isPlanar: true,
        colors,
        features: new FeatureIndex(),
        textureMapping: {
            texture,
            uvParams: [new Point2d(0, 1), new Point2d(1, 1), new Point2d(0, 0), new Point2d(1, 0)],
        },
    };
    return createMeshParams(quadArgs, viewport.target.renderSystem.maxTextureSize, "non-indexed" !== IModelApp.tileAdmin.edgeOptions.type);
}
function clampTransparency(transparency) {
    transparency = Math.min(255, transparency, Math.max(0, transparency));
    transparency = Math.floor(transparency);
    if (transparency < DisplayParams.minTransparency)
        transparency = 0;
    return transparency;
}
function computeRange(centroidRange, center, maxSize) {
    const range2 = centroidRange.clone();
    range2.low.subtractInPlace(center);
    range2.high.subtractInPlace(center);
    const halfSize = maxSize * 0.5;
    range2.low.x -= halfSize;
    range2.low.y -= halfSize;
    range2.low.z -= halfSize;
    range2.high.x += halfSize;
    range2.high.y += halfSize;
    range2.high.z += halfSize;
    return range2;
}
//# sourceMappingURL=ParticleCollectionBuilder.js.map