/*---------------------------------------------------------------------------------------------
* 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 } from "@itwin/core-bentley";
import { ClipUtilities, ConvexClipPlaneSet, Geometry, Map4d, Matrix3d, Matrix4d, Point3d, Range3d, Transform, Vector3d } from "@itwin/core-geometry";
import { Frustum, FrustumPlanes, RenderMode, RenderTexture, TextureTransparency, ViewFlags, } from "@itwin/core-common";
import { RenderType } from "@itwin/webgl-compatibility";
import { TileDrawArgs, TileVisibility } from "../../tile/internal";
import { BatchState } from "./BatchState";
import { EVSMGeometry } from "./CachedGeometry";
import { FrameBuffer } from "./FrameBuffer";
import { GL } from "./GL";
import { RenderCommands } from "./RenderCommands";
import { TextureUnit } from "./RenderFlags";
import { RenderState } from "./RenderState";
import { getDrawParams } from "./ScratchDrawParams";
import { System } from "./System";
import { Texture, TextureHandle } from "./Texture";
function createDrawArgs(sceneContext, solarShadowMap, tree, frustumPlanes, processTiles) {
    class SolarShadowMapDrawArgs extends TileDrawArgs {
        constructor(_mapFrustumPlanes, _shadowMap, args, process) {
            super(args);
            this._mapFrustumPlanes = _mapFrustumPlanes;
            this._shadowMap = _shadowMap;
            this._processTiles = process;
        }
        // The solar shadow projection is parallel - which can cause excessive tile selection if it is along an axis of an unbounded tile
        // tree such as the OSM buildings.  Rev limit the selection here.
        get maxRealityTreeSelectionCount() { return 500; }
        processSelectedTiles(tiles) {
            this._processTiles(tiles);
        }
        get frustumPlanes() {
            if (true === this._useViewportMap)
                return super.frustumPlanes;
            else
                return this._mapFrustumPlanes;
        }
        get worldToViewMap() {
            if (true === this._useViewportMap)
                return super.worldToViewMap;
            else
                return this._shadowMap.worldToViewMap;
        }
        drawGraphics() {
            const graphics = this.produceGraphics();
            if (graphics)
                this._shadowMap.addGraphic(graphics);
        }
        getPixelSize(tile) {
            // For tiles that are part of the scene, size them based on the viewport frustum so that shadow map uses same resolution tiles as scene
            // - otherwise artifacts like shadow acne may result.
            // For tiles that are NOT part of the scene, size them based on the shadow frustum, not the viewport frustum
            // - otherwise excessive numbers of excessively detailed may be requested for the shadow map.
            if (undefined === this._useViewportMap) {
                this._useViewportMap = true;
                const vis = tile.computeVisibility(this);
                this._useViewportMap = TileVisibility.OutsideFrustum !== vis;
            }
            const size = super.getPixelSize(tile);
            this._useViewportMap = undefined;
            return size;
        }
        static create(context, shadowMap, tileTree, planes, process) {
            const args = tileTree.createDrawArgs(context);
            return undefined !== args ? new SolarShadowMapDrawArgs(planes, shadowMap, args, process) : undefined;
        }
    }
    return SolarShadowMapDrawArgs.create(sceneContext, solarShadowMap, tree, frustumPlanes, processTiles);
}
const shadowMapWidth = 4096; // size of original depth buffer map
const shadowMapHeight = shadowMapWidth; // TBD - Adjust for aspect ratio.
const evsmWidth = shadowMapWidth / 2; // EVSM buffer is 1/2 size each direction
const evsmHeight = shadowMapHeight / 2;
const postProjectionMatrixNpc = Matrix4d.createRowValues(/* Row 1 */ 0, 1, 0, 0, /* Row 2 */ 0, 0, 1, 0, /* Row 3 */ 1, 0, 0, 0, /* Row 4 */ 0, 0, 0, 1);
// Bundles up the disposable, create-once-and-reuse members of a SolarShadowMap.
class Bundle {
    constructor(depthTexture, shadowMapTexture, fbo, fboSM, evsmGeom, renderCommands) {
        this.depthTexture = depthTexture;
        this.shadowMapTexture = shadowMapTexture;
        this.fbo = fbo;
        this.fboSM = fboSM;
        this.evsmGeom = evsmGeom;
        this.renderCommands = renderCommands;
    }
    static create(target, stack, batch) {
        const depthTextureHandle = System.instance.createDepthBuffer(shadowMapWidth, shadowMapHeight);
        if (undefined === depthTextureHandle)
            return undefined;
        let pixelDataType = GL.Texture.DataType.Float;
        switch (System.instance.maxRenderType) {
            case RenderType.TextureFloat:
                break;
            case RenderType.TextureHalfFloat:
                pixelDataType = System.instance.context.HALF_FLOAT;
                break;
            /* falls through */
            default:
                return undefined;
        }
        const colorTextures = [];
        const fbo = FrameBuffer.create(colorTextures, depthTextureHandle);
        if (undefined === fbo)
            return undefined;
        // shadowMap texture is 1/4 size the depth texture (and averaged down when converting)
        const shadowMapTextureHandle = TextureHandle.createForAttachment(evsmWidth, evsmHeight, GL.Texture.Format.Rgba, pixelDataType);
        if (undefined === shadowMapTextureHandle)
            return undefined;
        const fboSM = FrameBuffer.create([shadowMapTextureHandle]);
        if (undefined === fboSM)
            return undefined;
        const depthTexture = new Texture({ ownership: "external", type: RenderTexture.Type.TileSection, handle: depthTextureHandle, transparency: TextureTransparency.Opaque });
        const evsmGeom = EVSMGeometry.createGeometry(depthTexture.texture.getHandle(), shadowMapWidth, shadowMapHeight);
        if (undefined === evsmGeom)
            return undefined;
        const shadowMapTexture = new Texture({ type: RenderTexture.Type.Normal, ownership: "external", handle: shadowMapTextureHandle, transparency: TextureTransparency.Opaque });
        const renderCommands = new RenderCommands(target, stack, batch);
        return new Bundle(depthTexture, shadowMapTexture, fbo, fboSM, evsmGeom, renderCommands);
    }
    get isDisposed() {
        return this.depthTexture.isDisposed
            && this.shadowMapTexture.isDisposed
            && this.fbo.isDisposed
            && this.fboSM.isDisposed
            && this.evsmGeom.isDisposed;
    }
    dispose() {
        dispose(this.depthTexture);
        dispose(this.shadowMapTexture);
        dispose(this.fbo);
        dispose(this.fboSM);
        dispose(this.evsmGeom);
    }
}
/** Describes the set of parameters which, when they change, require us to recreate the shadow map. */
class ShadowMapParams {
    constructor(viewFrustum, direction, settings) {
        this.direction = new Vector3d();
        this.viewFrustum = new Frustum();
        direction.clone(this.direction);
        this.viewFrustum.setFrom(viewFrustum);
        this.settings = settings;
    }
    update(viewFrustum, direction, settings) {
        this.settings = settings;
        this.viewFrustum.setFrom(viewFrustum);
        direction.clone(this.direction);
    }
}
const defaultSunDirection = Vector3d.create(-1, -1, -1).normalize();
const scratchFrustum = new Frustum();
const scratchFrustumPlanes = FrustumPlanes.createEmpty();
export class SolarShadowMap {
    getBundle(target) {
        if (undefined === this._bundle) {
            this._bundle = Bundle.create(target, target.uniforms.branch.stack, this._batchState);
            assert(undefined !== this._bundle);
        }
        return this._bundle;
    }
    get isReady() { return this._isReady; }
    get isDrawing() { return this._isDrawing; }
    get isEnabled() { return this._enabled; }
    get projectionMatrix() { return this._projectionMatrix; }
    get depthTexture() { return undefined !== this._bundle ? this._bundle.depthTexture : undefined; }
    get shadowMapTexture() { return undefined !== this._bundle ? this._bundle.shadowMapTexture : undefined; }
    get settings() { return undefined !== this._params ? this._params.settings : undefined; }
    get direction() { return undefined !== this._params ? this._params.direction : undefined; }
    get frustum() { return this._shadowFrustum; }
    get worldToViewMap() { return this._worldToViewMap; }
    addGraphic(graphic) { this._graphics.push(graphic); }
    constructor(target) {
        this._projectionMatrix = Matrix4d.createIdentity();
        this._graphics = [];
        this._shadowFrustum = new Frustum();
        this._isReady = false;
        this._isDrawing = false;
        this._enabled = false;
        this._scratchRange = Range3d.createNull();
        this._scratchTransform = Transform.createIdentity();
        this._scratchViewFlags = new ViewFlags();
        this._worldToViewMap = Map4d.createIdentity();
        this._target = target;
        this._renderState = new RenderState();
        this._renderState.flags.depthMask = true;
        this._renderState.flags.blend = false;
        this._renderState.flags.depthTest = true;
        this._noZRenderState = new RenderState();
        this._noZRenderState.flags.depthMask = false;
        this._batchState = new BatchState(target.uniforms.branch.stack);
    }
    disable() {
        this._enabled = this._isReady = false;
        this._bundle = dispose(this._bundle);
        this.clearGraphics(true);
        this._target.uniforms.shadow.update();
    }
    collectStatistics(stats) {
        const bundle = this._bundle;
        if (undefined !== bundle)
            stats.addShadowMap(bundle.depthTexture.bytesUsed + bundle.shadowMapTexture.bytesUsed);
    }
    get isDisposed() { return undefined === this._bundle && 0 === this._graphics.length; }
    dispose() {
        this._bundle = dispose(this._bundle);
        this.clearGraphics(true);
    }
    clearGraphics(notify) {
        for (const graphic of this._graphics)
            graphic.dispose();
        this._graphics.length = 0;
        if (notify)
            this.notifyGraphicsChanged();
    }
    notifyGraphicsChanged() {
        if (undefined !== this.onGraphicsChanged)
            this.onGraphicsChanged(this._graphics);
    }
    update(context) {
        this._isReady = false;
        this.clearGraphics(false);
        if (undefined === context || !context.viewport.view.isSpatialView()) {
            this.disable();
            this.notifyGraphicsChanged();
            return;
        }
        const view = context.viewport.view;
        const style = view.getDisplayStyle3d();
        const sunDirection = style.sunDirection ?? defaultSunDirection;
        const minimumHorizonDirection = -.01;
        if (sunDirection.z > minimumHorizonDirection) {
            this.notifyGraphicsChanged();
            return;
        }
        this._enabled = true;
        const viewFrustum = context.viewingSpace.getFrustum();
        const settings = style.settings.solarShadows;
        if (undefined === this._params)
            this._params = new ShadowMapParams(viewFrustum, sunDirection, settings);
        else
            this._params.update(viewFrustum, sunDirection, settings);
        const iModel = view.iModel;
        const worldToMapTransform = Transform.createRefs(Point3d.createZero(), Matrix3d.createRigidHeadsUp(this._params.direction.negate()).inverse());
        const worldToMap = Matrix4d.createTransform(worldToMapTransform);
        const mapToWorld = worldToMap.createInverse();
        // Start with entire project.
        const shadowRange = worldToMapTransform.multiplyRange(iModel.projectExtents);
        // Limit the map to only displayed models.
        const viewTileRange = Range3d.createNull();
        view.forEachTileTreeRef((ref) => {
            if (ref.castsShadows) {
                if (ref.isGlobal) {
                    // A shadow-casting tile tree that spans the globe. Limit its range to the viewed extents.
                    for (const p3 of viewFrustum.points) {
                        const p4 = worldToMap.multiplyPoint3d(p3, 1);
                        if (p4.w > 0.0001)
                            viewTileRange.extendXYZW(p4.x, p4.y, p4.z, p4.w);
                        else
                            viewTileRange.high.z = Math.max(1.0, viewTileRange.high.z); // behind eye plane.
                    }
                }
                else {
                    ref.accumulateTransformedRange(viewTileRange, worldToMap, undefined);
                }
            }
        });
        if (!viewTileRange.isNull)
            viewTileRange.clone(shadowRange);
        // Expand shadow range to include both the shadowers and shadowed portion of background map.
        scratchFrustum.initFromRange(shadowRange);
        mapToWorld.multiplyPoint3dArrayQuietNormalize(scratchFrustum.points); // This frustum represents the shadowing geometry.  Intersect it with background geometry and expand the range depth to include that intersection.
        const backgroundMapGeometry = context.viewport.view.displayStyle.getBackgroundMapGeometry();
        if (undefined !== backgroundMapGeometry) {
            const backgroundDepthRange = backgroundMapGeometry.getFrustumIntersectionDepthRange(this._shadowFrustum, iModel.projectExtents);
            if (!backgroundDepthRange.isNull)
                shadowRange.low.z = Math.min(shadowRange.low.z, backgroundDepthRange.low);
        }
        this._params.viewFrustum.transformBy(worldToMapTransform, scratchFrustum);
        scratchFrustumPlanes.init(scratchFrustum);
        const viewIntersectShadowRange = Range3d.createNull();
        const viewClipPlanes = ConvexClipPlaneSet.createPlanes(scratchFrustumPlanes.planes);
        ClipUtilities.announceLoopsOfConvexClipPlaneSetIntersectRange(viewClipPlanes, shadowRange, (points) => {
            for (const point of points.getPoint3dArray())
                viewIntersectShadowRange.extendPoint(point);
        });
        if (viewIntersectShadowRange.isNull) {
            this.notifyGraphicsChanged();
            return;
        }
        viewIntersectShadowRange.high.z = shadowRange.high.z; // We need to include shadowing geometry that may be outside view (along the solar axis).
        this._shadowFrustum.initFromRange(viewIntersectShadowRange);
        mapToWorld.multiplyPoint3dArrayQuietNormalize(this._shadowFrustum.points);
        const tileRange = Range3d.createNull();
        scratchFrustumPlanes.init(this._shadowFrustum);
        view.forEachTileTreeRef(((ref) => {
            if (!ref.castsShadows)
                return;
            const drawArgs = createDrawArgs(context, this, ref, scratchFrustumPlanes, (tiles) => {
                for (const tile of tiles)
                    tileRange.extendRange(tileToMapTransform.multiplyRange(tile.range, this._scratchRange));
            });
            if (undefined === drawArgs)
                return;
            const tileToMapTransform = worldToMapTransform.multiplyTransformTransform(drawArgs.location, this._scratchTransform);
            drawArgs.tree.draw(drawArgs);
        }));
        if (tileRange.isNull) {
            this.clearGraphics(true);
        }
        else if (this._graphics.length > 0) {
            // Avoid an uninvertible matrix on empty range...
            if (Geometry.isAlmostEqualNumber(shadowRange.low.x, shadowRange.high.x) ||
                Geometry.isAlmostEqualNumber(shadowRange.low.y, shadowRange.high.y) ||
                Geometry.isAlmostEqualNumber(shadowRange.low.z, shadowRange.high.z)) {
                this.clearGraphics(true);
                return;
            }
            const frustumMap = this._shadowFrustum.toMap4d();
            if (undefined === frustumMap) {
                this.clearGraphics(true);
                assert(false);
                return;
            }
            this._projectionMatrix = frustumMap.transform0.clone();
            const worldToNpc = postProjectionMatrixNpc.multiplyMatrixMatrix(this._projectionMatrix);
            const npcToView = Map4d.createBoxMap(Point3d.create(0, 0, 0), Point3d.create(1, 1, 1), Point3d.create(0, 0, 0), Point3d.create(shadowMapWidth, shadowMapHeight, 1));
            const npcToWorld = worldToNpc.createInverse();
            if (undefined === npcToWorld) {
                this.clearGraphics(true);
                return;
            }
            const worldToNpcMap = Map4d.createRefs(worldToNpc, npcToWorld);
            this._worldToViewMap = npcToView.multiplyMapMap(worldToNpcMap);
        }
        this._target.uniforms.shadow.update();
        this.notifyGraphicsChanged();
    }
    draw(target) {
        assert(this.isEnabled);
        if (this.isReady || 0 === this._graphics.length)
            return;
        const bundle = this.getBundle(target);
        if (undefined === bundle)
            return;
        this._isDrawing = true;
        const prevState = System.instance.currentRenderState.clone();
        const gl = System.instance.context;
        gl.viewport(0, 0, shadowMapWidth, shadowMapHeight);
        // NB: textures and materials are needed because their transparencies affect whether or not a surface casts shadows
        const viewFlags = target.currentViewFlags.copy({
            renderMode: RenderMode.SmoothShade,
            wiremesh: false,
            transparency: false,
            lighting: false,
            shadows: false,
            monochrome: false,
            ambientOcclusion: false,
            visibleEdges: false,
            hiddenEdges: false,
        });
        System.instance.applyRenderState(this._renderState);
        const prevPlan = target.plan;
        target.changeFrustum(this._shadowFrustum, this._shadowFrustum.getFraction(), true);
        target.uniforms.branch.changeRenderPlan(viewFlags, target.plan.is3d, target.plan.hline);
        const renderCommands = bundle.renderCommands;
        renderCommands.reset(target, target.uniforms.branch.stack, this._batchState);
        renderCommands.addGraphics(this._graphics);
        System.instance.frameBufferStack.execute(bundle.fbo, true, false, () => {
            System.instance.context.clearDepth(1.0);
            System.instance.context.clear(GL.BufferBit.Depth);
            target.techniques.execute(target, renderCommands.getCommands(3 /* RenderPass.OpaquePlanar */), 19 /* RenderPass.PlanarClassification */); // Draw these with RenderPass.PlanarClassification (rather than Opaque...) so that the pick ordering is avoided.
            target.techniques.execute(target, renderCommands.getCommands(5 /* RenderPass.OpaqueGeneral */), 19 /* RenderPass.PlanarClassification */); // Draw these with RenderPass.PlanarClassification (rather than Opaque...) so that the pick ordering is avoided.
        });
        // copy depth buffer to EVSM shadow buffer and average down for AA effect
        gl.viewport(0, 0, evsmWidth, evsmHeight);
        System.instance.frameBufferStack.execute(bundle.fboSM, true, false, () => {
            System.instance.applyRenderState(this._noZRenderState);
            const params = getDrawParams(target, bundle.evsmGeom);
            target.techniques.draw(params);
        });
        // mipmap resulting EVSM texture and set filtering options
        System.instance.activateTexture2d(TextureUnit.ShadowMap, bundle.shadowMapTexture.texture.getHandle());
        gl.generateMipmap(gl.TEXTURE_2D);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
        System.instance.setMaxAnisotropy(undefined);
        // target.recordPerformanceMetric("Compute EVSM");
        this._batchState.reset(); // Reset the batch Ids...
        target.changeRenderPlan(prevPlan);
        System.instance.applyRenderState(prevState);
        System.instance.context.viewport(0, 0, target.viewRect.width, target.viewRect.height); // Restore viewport
        this.clearGraphics(false);
        this._isDrawing = false;
        this._isReady = true;
    }
}
//# sourceMappingURL=SolarShadowMap.js.map