/** @internal NOTE: Internal APIs. Subject to change. Use of these APIs in production applications is not supported. */ /** */

import { TwinfinityCameraBehavior } from '../camera/TwinfinityCameraBehavior';
import {
    BlurPostProcess,
    PassPostProcess,
    Camera,
    Vector3,
    RenderTargetTexture,
    AbstractMesh,
    Mesh,
    AbstractEngine,
    Scene,
    Vector2,
    Color4
} from '../loader/babylonjs-import';
import { OutlineCalculator } from './OutlineCalculator';
import { OutlineApplier } from './OutlineApplier';
import { copySizeTo } from './CopySizeFunctions';

/**
 * Creates an instance of of the Outline effect postprocess, should be created using createOutlinePipeline from PostProcessEffects
 */
export class OutlinePipeline implements TwinfinityCameraBehavior {
    private _outlineCalculator: OutlineCalculator;
    private _outlineApplier: OutlineApplier;
    private _horizontalBlur: BlurPostProcess;
    private _verticalBlur: BlurPostProcess;
    private _screenBlit: PassPostProcess;
    private _engine: AbstractEngine;
    private _scene: Scene;
    private _outlineTexture: RenderTargetTexture;

    attach(camera: Camera): boolean {
        this._screenBlit = new PassPostProcess('screen blit', 1.0, camera);

        this._engine = camera.getEngine();
        this._scene = camera.getScene();

        this._outlineTexture = this.createOutlineRenderTarget(camera);

        this._outlineCalculator = new OutlineCalculator(
            this._outlineTexture,
            this._renderScale,
            this._unifiedOutline,
            this._outlineColor
        );

        const outlineAttached = this._outlineCalculator.attach(camera);

        // Scale the blur with the renderscale
        const blurSize = 8;
        this._horizontalBlur = new BlurPostProcess(
            'Horizontal blur',
            new Vector2(1.0, 0),
            blurSize,
            this._renderScale,
            camera
        );
        this._verticalBlur = new BlurPostProcess(
            'Vertical blur',
            new Vector2(0, 1.0),
            blurSize,
            this._renderScale,
            camera
        );

        this._outlineApplier = new OutlineApplier(this._screenBlit, this._renderScale);

        this._outlineApplier.attach(camera);

        return outlineAttached;
    }

    detach(camera: Camera): boolean {
        this._outlineApplier.detach(camera);
        this._outlineCalculator.detach(camera);
        camera.detachPostProcess(this._horizontalBlur);
        camera.detachPostProcess(this._verticalBlur);
        camera.detachPostProcess(this._screenBlit);

        // Remove from customRenderTargets before disposing to prevent memory leak
        const index = this._scene.customRenderTargets.indexOf(this._outlineTexture);
        if (index > -1) {
            this._scene.customRenderTargets.splice(index, 1);
        }

        this._outlineTexture.dispose();
        return true;
    }

    /**
     * Creates a render target that for every frame draws all outlines object into a framebuffer
     *
     * @returns The render target texture
     */
    private createOutlineRenderTarget(camera: Camera): RenderTargetTexture {
        const outLineRenderTarget = 'outLineRenderTarget';

        const width = Math.floor(this._engine.getRenderWidth(true) * this._renderScale);
        const height = Math.floor(this._engine.getRenderHeight(true) * this._renderScale);

        const outlinesIdTextures = new RenderTargetTexture(
            outLineRenderTarget,
            {
                width: width,
                height: height
            },
            this._scene
        );

        outlinesIdTextures.onBeforeBindObservable.add((texture) =>
            copySizeTo({
                src: texture.getScene()!.getEngine(),
                dst: texture,
                scale: 0.5
            })
        );

        outlinesIdTextures.clearColor = new Color4(0.0, 0.0, 0.0, 1.0);
        this._scene.customRenderTargets.push(outlinesIdTextures);

        const viewer = camera.getScene().twinfinity.viewer;

        if (viewer) {
            // Filter away all meshes that are not outlined
            outlinesIdTextures.renderListPredicate = (mesh: AbstractMesh): boolean => {
                if (mesh instanceof Mesh) {
                    const meshIncluded = this.includeMesh(mesh);

                    if (meshIncluded) {
                        const outlineMaterial = viewer._materials.getOutlineMaterial(mesh);
                        outlinesIdTextures.setMaterialForRendering(mesh, outlineMaterial);
                    }

                    return meshIncluded;
                }

                return false;
            };

            return outlinesIdTextures;
        } else {
            throw new Error(
                "Can't attach this postprocess to a camera that does belong to a scene in a Twinfinity viewer"
            );
        }
    }

    constructor(
        private readonly _renderScale: number,
        private readonly _unifiedOutline: boolean,
        private readonly _outlineColor: Vector3
    ) {}

    private includeMesh(mesh: Mesh): boolean {
        // NOTE: Order of comparisons is important here because mesh.getBoundingInfo() can come with a hefty
        // peformance penalty when called for the first time. This often happens when we have a lot if non IFC meshes. Since we are only interested
        // in IFC meshes we put those checks first (as they are cheap).
        return (
            mesh.twinfinity?.ifc != null &&
            mesh.twinfinity?.ifc.productMeshRenderInfo.state.outlinedCount > 0 &&
            //  There appears to be some kind of race condition where it might render before there is boundingInfo available
            mesh.getBoundingInfo() !== undefined
        );
    }
}
