/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module WebGL
 */
import { assert, dispose, disposeArray } from "@itwin/core-bentley";
import { Quantization } from "@itwin/core-common";
import { Range2d, Range3d, Transform, Vector2d } from "@itwin/core-geometry";
import { GraphicBranch } from "../GraphicBranch";
import { TerrainTexture } from "../RenderSystem";
import { BufferHandle, BufferParameters, QBufferHandle2d, QBufferHandle3d } from "./AttributeBuffers";
import { AttributeMap } from "./AttributeMap";
import { IndexedGeometry, IndexedGeometryParams } from "./CachedGeometry";
import { GL } from "./GL";
import { Matrix4 } from "./Matrix";
import { Primitive } from "./Primitive";
import { System } from "./System";
const scratchOverlapRange = Range2d.createNull();
const scratchBytes = new Uint8Array(4);
const scratchBatchBaseId = new Uint32Array(scratchBytes.buffer);
const scratchRange2d = Range2d.createNull();
class ProjectedTexture {
    constructor(classifier, meshParams, targetRectangle) {
        this.meshParams = meshParams;
        this.targetRectangle = targetRectangle;
        this.classifier = classifier;
    }
    clone(targetRectangle) {
        return new ProjectedTexture(this.classifier, this.meshParams, targetRectangle.clone());
    }
}
class RealityTextureParam {
    constructor(texture, _projectedTextureOrMatrix) {
        this.texture = texture;
        this._projectedTextureOrMatrix = _projectedTextureOrMatrix;
    }
    get isProjected() { return this._projectedTextureOrMatrix instanceof ProjectedTexture; }
    dispose() {
        this.texture = dispose(this.texture);
    }
    /* There are two methods of applying a texture to a reality mesh.  the first member of "params" denotes which
    method is to be used.  A value of zero indicates a standard texture and one represents a classified texture.
  
    A standard (nonprojected) texture is generated by multiplying v_textCoord by the scaling and translation packed into the first row
    of "matrix". A clip rectangle is packed into second row of "matrix".
  
    A "classified" reality mesh texture is used for map layers.  It does not uses v_texCoord, the texture coordinates
    are instead generated by a projection of the model position onto the X-Y plane. We only have eye position, not model position
    so the matrix in this case is a real transform matrix that contains a mapping from eye to model position
    followed by the model to texture projection.
  */
    getProjectionMatrix() {
        return this._projectedTextureOrMatrix instanceof ProjectedTexture ? this._projectedTextureOrMatrix.classifier.projectionMatrix : undefined;
    }
    getTerrainMatrix() {
        return this._projectedTextureOrMatrix instanceof Matrix4 ? this._projectedTextureOrMatrix : undefined;
    }
    getParams(result) {
        /** Entry 0 is 0 for  */
        if (this._projectedTextureOrMatrix instanceof ProjectedTexture) {
            const projectedTexture = this._projectedTextureOrMatrix;
            result.data[0] = 1;
            result.data[1] = projectedTexture.classifier.textureImageCount;
            result.data[2] = projectedTexture.classifier.sourceTransparency === undefined ? 1.0 : (1.0 - projectedTexture.classifier.sourceTransparency);
            scratchBatchBaseId[0] = projectedTexture.classifier.baseBatchId;
            result.data[4] = scratchBytes[0];
            result.data[5] = scratchBytes[1];
            result.data[6] = scratchBytes[2];
            result.data[7] = scratchBytes[3];
            const points = [];
            const meshParams = projectedTexture.meshParams;
            // Calculate range in the tiles local coordinates.
            const low = meshParams.tileRectangle.worldToLocal(projectedTexture.targetRectangle.low, scratchRange2d.low);
            const high = meshParams.tileRectangle.worldToLocal(projectedTexture.targetRectangle.high, scratchRange2d.high);
            points.push(meshParams.projection.getGlobalPoint(low.x, low.y, 0));
            points.push(meshParams.projection.getGlobalPoint(high.x, low.y, 0));
            points.push(meshParams.projection.getGlobalPoint(high.x, high.y, 0));
            points.push(meshParams.projection.getGlobalPoint(low.x, high.y, 0));
            for (let i = 0, j = 8; i < 4; i++) {
                const projectedPoint = projectedTexture.classifier.projectionMatrix.multiplyPoint3dQuietNormalize(points[i]);
                result.data[j++] = projectedPoint.x;
                result.data[j++] = projectedPoint.y;
            }
            const x0 = result.data[10] - result.data[8], y0 = result.data[11] - result.data[9];
            const x1 = result.data[12] - result.data[8], y1 = result.data[13] - result.data[9];
            if (x0 * y1 - x1 * y0 < 0) {
                const swap = ((i, j) => {
                    const temp = result.data[i];
                    result.data[i] = result.data[j];
                    result.data[j] = temp;
                });
                for (let i = 8, j = 14; i <= 10; i += 2, j -= 2) {
                    swap(i, j);
                    swap(i + 1, j + 1);
                }
            }
        }
        else {
            result.data[0] = 0;
        }
        return result;
    }
}
/** @internal */
export class RealityTextureParams {
    constructor(params) {
        this.params = params;
    }
    static create(textures) {
        const maxTexturesPerMesh = System.instance.maxRealityImageryLayers;
        assert(textures.length <= maxTexturesPerMesh);
        const textureParams = new Array();
        for (const texture of textures) {
            if (texture instanceof TerrainTexture) {
                const terrainTexture = texture;
                const matrix = new Matrix4(); // Published as Mat4.
                assert(terrainTexture.texture !== undefined, "Texture not defined in TerrainTextureParams constructor");
                matrix.data[0] = terrainTexture.translate.x;
                matrix.data[1] = terrainTexture.translate.y;
                matrix.data[2] = terrainTexture.scale.x;
                matrix.data[3] = terrainTexture.scale.y;
                if (terrainTexture.clipRectangle) {
                    matrix.data[4] = terrainTexture.clipRectangle.low.x;
                    matrix.data[5] = terrainTexture.clipRectangle.low.y;
                    matrix.data[6] = terrainTexture.clipRectangle.high.x;
                    matrix.data[7] = terrainTexture.clipRectangle.high.y;
                }
                else {
                    matrix.data[4] = matrix.data[5] = 0;
                    matrix.data[6] = matrix.data[7] = 1;
                }
                matrix.data[8] = (1.0 - terrainTexture.transparency);
                matrix.data[9] = terrainTexture.featureId;
                textureParams.push(new RealityTextureParam(terrainTexture.texture, matrix));
            }
            else {
                const classifier = texture.classifier;
                textureParams.push(new RealityTextureParam(classifier.getOrCreateClassifierTexture(), texture));
            }
        }
        for (let i = textures.length; i < maxTexturesPerMesh; i++) {
            const matrix = new Matrix4();
            matrix.data[0] = matrix.data[1] = 0.0;
            matrix.data[2] = matrix.data[3] = 1.0;
            matrix.data[4] = matrix.data[5] = 1;
            matrix.data[6] = matrix.data[7] = -1;
            matrix.data[15] = 0; // Denotes a terrain texture.
            textureParams.push(new RealityTextureParam(undefined, matrix));
        }
        return new RealityTextureParams(textureParams);
    }
    dispose() {
        disposeArray(this.params);
    }
}
/** @internal */
export class RealityMeshGeometryParams extends IndexedGeometryParams {
    constructor(positions, normals, uvParams, indices, numIndices, numBytesPerIndex, featureID) {
        super(positions, indices, numIndices);
        this.numBytesPerIndex = numBytesPerIndex;
        let attrParams = AttributeMap.findAttribute("a_uvParam", 7 /* TechniqueId.RealityMesh */, false);
        assert(attrParams !== undefined);
        this.buffers.addBuffer(uvParams, [BufferParameters.create(attrParams.location, 2, GL.DataType.UnsignedShort, false, 0, 0, false)]);
        this.uvParams = uvParams;
        if (undefined !== normals) {
            attrParams = AttributeMap.findAttribute("a_norm", 7 /* TechniqueId.RealityMesh */, false);
            assert(attrParams !== undefined);
            if (normals.bytesUsed > 0)
                this.buffers.addBuffer(normals, [BufferParameters.create(attrParams.location, 2, GL.DataType.UnsignedByte, false, 0, 0, false)]);
            this.normals = normals;
        }
        this.featureID = featureID;
    }
    static createFromBuffers(posBuf, uvParamBuf, indices, normBuf, featureID) {
        const indBuf = BufferHandle.createBuffer(GL.Buffer.Target.ElementArrayBuffer, indices);
        if (undefined === indBuf)
            return undefined;
        const bytesPerIndex = indices.BYTES_PER_ELEMENT;
        assert(1 === bytesPerIndex || 2 === bytesPerIndex || 4 === bytesPerIndex);
        return new RealityMeshGeometryParams(posBuf, normBuf, uvParamBuf, indBuf, indices.length, bytesPerIndex, featureID);
    }
    static fromRealityMesh(params) {
        const posBuf = QBufferHandle3d.create(params.positions.params, params.positions.points);
        const uvParamBuf = QBufferHandle2d.create(params.uvs.params, params.uvs.points);
        const normalBuf = params.normals ? BufferHandle.createArrayBuffer(params.normals) : undefined;
        return (undefined === posBuf || undefined === uvParamBuf) ? undefined : this.createFromBuffers(posBuf, uvParamBuf, params.indices, normalBuf, params.featureID ?? 0);
    }
    get isDisposed() {
        return super.isDisposed && this.uvParams.isDisposed;
    }
    get bytesUsed() { return this.positions.bytesUsed + (undefined === this.normals ? 0 : this.normals.bytesUsed) + this.uvParams.bytesUsed + this.indices.bytesUsed; }
    dispose() {
        super.dispose();
        dispose(this.uvParams);
    }
}
/** @internal */
export class RealityMeshGeometry extends IndexedGeometry {
    get asRealityMesh() { return this; }
    get isDisposed() { return this._realityMeshParams.isDisposed; }
    get uvQParams() { return this._realityMeshParams.uvParams.params; }
    get hasFeatures() { return this._realityMeshParams.featureID !== undefined; }
    get supportsThematicDisplay() { return true; }
    get overrideColorMix() { return .5; } // This could be a setting from either the mesh or the override if required.
    get transform() { return this._transform; }
    constructor(props) {
        super(props.realityMeshParams);
        this.renderGeometryType = "reality-mesh";
        this.isInstanceable = false;
        this.noDispose = false;
        this._realityMeshParams = props.realityMeshParams;
        this.textureParams = props.textureParams;
        this._transform = props.transform;
        this.baseColor = props.baseColor;
        this._baseIsTransparent = props.baseIsTransparent;
        this._isTerrain = props.isTerrain;
        this._disableTextureDisposal = props.disableTextureDisposal;
        this.hasTextures = undefined !== this.textureParams && this.textureParams.params.some((x) => undefined !== x.texture);
        const bytesPerIndex = props.realityMeshParams.numBytesPerIndex;
        this._indexType = 1 === bytesPerIndex ? GL.DataType.UnsignedByte : (2 === bytesPerIndex ? GL.DataType.UnsignedShort : GL.DataType.UnsignedInt);
    }
    dispose() {
        if (this.noDispose) {
            return;
        }
        super.dispose();
        dispose(this._realityMeshParams);
        if (true !== this._disableTextureDisposal)
            dispose(this.textureParams);
    }
    static createForTerrain(mesh, transform, disableTextureDisposal = false) {
        const params = RealityMeshGeometryParams.fromRealityMesh(mesh);
        if (!params)
            return undefined;
        return new RealityMeshGeometry({
            realityMeshParams: params,
            transform,
            baseIsTransparent: false,
            isTerrain: true,
            disableTextureDisposal,
        });
    }
    static createFromRealityMesh(realityMesh, disableTextureDisposal = false) {
        const params = RealityMeshGeometryParams.fromRealityMesh(realityMesh);
        if (!params)
            return undefined;
        const texture = realityMesh.texture ? new TerrainTexture(realityMesh.texture, realityMesh.featureID ?? 0, Vector2d.create(1.0, -1.0), Vector2d.create(0.0, 1.0), Range2d.createXYXY(0, 0, 1, 1), 0, 0) : undefined;
        return new RealityMeshGeometry({ realityMeshParams: params, textureParams: texture ? RealityTextureParams.create([texture]) : undefined, baseIsTransparent: false, isTerrain: false, disableTextureDisposal });
    }
    getRange() {
        return Range3d.createXYZXYZ(this.qOrigin[0], this.qOrigin[1], this.qOrigin[2], this.qOrigin[0] + Quantization.rangeScale16 * this.qScale[0], this.qOrigin[1] + Quantization.rangeScale16 * this.qScale[1], this.qOrigin[2] + Quantization.rangeScale16 * this.qScale[2]);
    }
    static createGraphic(system, params, disableTextureDisposal = false) {
        const meshes = [];
        const textures = params.textures ?? [];
        const realityMesh = params.realityMesh;
        const { baseColor, baseTransparent, featureTable, tileId, layerClassifiers } = params;
        const texturesPerMesh = System.instance.maxRealityImageryLayers;
        const layers = new Array();
        // Collate the textures and classifiers layers into a single array.
        for (const texture of textures) {
            const layer = layers[texture.layerIndex];
            if (layer) {
                layer.push(texture);
            }
            else {
                layers[texture.layerIndex] = [texture];
            }
        }
        params.layerClassifiers?.forEach((layerClassifier, layerIndex) => layers[layerIndex] = [new ProjectedTexture(layerClassifier, params, params.tileRectangle)]);
        if (layers.length < 2 && !layerClassifiers?.size && textures.length < texturesPerMesh) {
            // If only there is not more than one layer then we can group all of the textures into a single draw call.
            meshes.push(new RealityMeshGeometry({ realityMeshParams: realityMesh._realityMeshParams, textureParams: RealityTextureParams.create(textures), transform: realityMesh._transform, baseColor, baseIsTransparent: baseTransparent, isTerrain: realityMesh._isTerrain, disableTextureDisposal }));
        }
        else {
            let primaryLayer;
            while (primaryLayer === undefined)
                primaryLayer = layers.shift();
            if (!primaryLayer)
                return undefined;
            for (const primaryTexture of primaryLayer) {
                const targetRectangle = primaryTexture.targetRectangle;
                const overlapMinimum = 1.0E-5 * (targetRectangle.high.x - targetRectangle.low.x) * (targetRectangle.high.y - targetRectangle.low.y);
                let layerTextures = [primaryTexture];
                for (const secondaryLayer of layers) {
                    if (!secondaryLayer)
                        continue;
                    for (const secondaryTexture of secondaryLayer) {
                        if (secondaryTexture instanceof ProjectedTexture) {
                            layerTextures.push(secondaryTexture.clone(targetRectangle));
                        }
                        else {
                            const secondaryRectangle = secondaryTexture.targetRectangle;
                            const overlap = targetRectangle.intersect(secondaryRectangle, scratchOverlapRange);
                            if (!overlap.isNull && (overlap.high.x - overlap.low.x) * (overlap.high.y - overlap.low.y) > overlapMinimum) {
                                const textureRange = Range2d.createXYXY(overlap.low.x, overlap.low.y, overlap.high.x, overlap.high.y);
                                secondaryRectangle.worldToLocal(textureRange.low, textureRange.low);
                                secondaryRectangle.worldToLocal(textureRange.high, textureRange.high);
                                if (secondaryTexture.clipRectangle)
                                    textureRange.intersect(secondaryTexture.clipRectangle, textureRange);
                                if (!textureRange.isNull && textureRange) {
                                    layerTextures.push(secondaryTexture.cloneWithClip(textureRange));
                                }
                            }
                        }
                    }
                }
                while (layerTextures.length > texturesPerMesh) {
                    meshes.push(new RealityMeshGeometry({ realityMeshParams: realityMesh._realityMeshParams, textureParams: RealityTextureParams.create(layerTextures.slice(0, texturesPerMesh)), transform: realityMesh._transform, baseColor, baseIsTransparent: baseTransparent, isTerrain: realityMesh._isTerrain, disableTextureDisposal }));
                    layerTextures = layerTextures.slice(texturesPerMesh);
                }
                meshes.push(new RealityMeshGeometry({ realityMeshParams: realityMesh._realityMeshParams, textureParams: RealityTextureParams.create(layerTextures), transform: realityMesh._transform, baseColor, baseIsTransparent: baseTransparent, isTerrain: realityMesh._isTerrain, disableTextureDisposal }));
            }
        }
        if (meshes.length === 0)
            return undefined;
        const branch = new GraphicBranch(true);
        for (const mesh of meshes) {
            const primitive = Primitive.create(mesh);
            branch.add(system.createBatch(primitive, featureTable, mesh.getRange(), { tileId }));
        }
        return system.createBranch(branch, realityMesh._transform ? realityMesh._transform : Transform.createIdentity(), { disableClipStyle: params.disableClipStyle });
    }
    collectStatistics(stats) {
        this._isTerrain ? stats.addTerrain(this._realityMeshParams.bytesUsed) : stats.addRealityMesh(this._realityMeshParams.bytesUsed);
        if (this.textureParams?.params) {
            for (const param of this.textureParams.params) {
                if (param.texture?.bytesUsed)
                    stats.addTexture(param.texture.bytesUsed);
            }
        }
    }
    get techniqueId() { return 7 /* TechniqueId.RealityMesh */; }
    getPass(target) {
        if (this._baseIsTransparent || (target.wantThematicDisplay && target.uniforms.thematic.wantIsoLines))
            return "translucent";
        return "opaque";
    }
    get renderOrder() { return 3 /* RenderOrder.UnlitSurface */; }
    draw() {
        this._params.buffers.bind();
        System.instance.context.drawElements(GL.PrimitiveType.Triangles, this._params.numIndices, this._indexType, 0);
        this._params.buffers.unbind();
    }
}
//# sourceMappingURL=RealityMesh.js.map