import {
    Color,
    NoBlending,
    RGBAFormat,
    ShaderMaterial,
    UniformsUtils,
    Vector2,
    WebGLMultisampleRenderTarget
} from "three";
import { BlurShaderUtils, DepthLimitedBlurShader } from "three/examples/jsm/shaders/DepthLimitedBlurShader.js";
import { renderScreenSpace } from "../../utils/renderScreenSpace.js";

const scratch_color = new Color();

export class DepthLimitedBlur {
    constructor({ format = RGBAFormat, radius = 8, clear_color = 0, clear_alpha = 1 }) {

        /**
         *
         * @type {WebGLRenderTarget|null}
         * @private
         */
        this.__rt_input = null;

        /**
         *
         * @type {WebGLRenderTarget|null}
         * @private
         */
        this.__rt_output = null;

        this.__intermediate = new WebGLMultisampleRenderTarget({ format });


        this.__material_blur_x = new ShaderMaterial({
            uniforms: UniformsUtils.clone(DepthLimitedBlurShader.uniforms),
            defines: Object.assign({}, DepthLimitedBlurShader.defines),
            vertexShader: DepthLimitedBlurShader.vertexShader,
            fragmentShader: DepthLimitedBlurShader.fragmentShader,
            blending: NoBlending
        });

        this.__material_blur_y = new ShaderMaterial({
            uniforms: UniformsUtils.clone(DepthLimitedBlurShader.uniforms),
            defines: Object.assign({}, DepthLimitedBlurShader.defines),
            vertexShader: DepthLimitedBlurShader.vertexShader,
            fragmentShader: DepthLimitedBlurShader.fragmentShader,
            blending: NoBlending
        });


        this.__depth_cutoff = 0.01;

        this.__blur_radius = radius;
        this.__blur_std_dev = 4;

        this.__clear_color = new Color(clear_color);
        this.__clear_alpha = clear_alpha;
    }

    configureMaterials() {
        const mx = this.__material_blur_x;
        const my = this.__material_blur_y;

        mx.defines.DEPTH_PACKING = my.defines.DEPTH_PACKING = 0;
        mx.defines.PERSPECTIVE_CAMERA = my.defines.PERSPECTIVE_CAMERA = 1;

        mx.blending = my.blending = NoBlending;

        BlurShaderUtils.configure(mx, this.__blur_radius, this.__blur_std_dev, new Vector2(1, 0));
        BlurShaderUtils.configure(my, this.__blur_radius, this.__blur_std_dev, new Vector2(0, 1));


        this.__material_blur_y.uniforms.tDiffuse.value = this.__intermediate.texture;
    }

    /**
     *
     * @param {Camera} camera
     */
    setRenderCamera(camera) {
        const mx = this.__material_blur_x;
        const my = this.__material_blur_y;

        if (camera.isPerspectiveCamera === true && mx.defines.PERSPECTIVE_CAMERA !== 1) {
            mx.defines.PERSPECTIVE_CAMERA = 1;
            my.defines.PERSPECTIVE_CAMERA = 1;

            mx.needsUpdate = true;
            my.needsUpdate = true;
        } else if (camera.isPerspectiveCamera !== true && mx.defines.PERSPECTIVE_CAMERA !== 0) {
            mx.defines.PERSPECTIVE_CAMERA = 0;
            my.defines.PERSPECTIVE_CAMERA = 0;

            mx.needsUpdate = true;
            my.needsUpdate = true;
        }

        const depthCutoff = this.__depth_cutoff * (camera.far - camera.near);

        mx.uniforms.depthCutoff.value = depthCutoff;
        my.uniforms.depthCutoff.value = depthCutoff;

        mx.uniforms.cameraNear.value = my.uniforms.cameraNear.value = camera.near;
        mx.uniforms.cameraFar.value = my.uniforms.cameraFar.value = camera.far;
    }


    /**
     *
     * @param {WebGLRenderer} renderer
     */
    execute(renderer) {
        // update intermediate buffer size based on input size
        if (
            this.__intermediate.width !== this.__rt_output.width
            || this.__intermediate.height !== this.__rt_output.height
        ) {
            this.__intermediate.setSize(this.__rt_output.width, this.__rt_output.height);
        }

        const mx = this.__material_blur_x;
        const my = this.__material_blur_y;

        // set resolution parameter for each shader
        mx.uniforms.size.value.set(this.__rt_output.width, this.__rt_output.height);
        my.uniforms.size.value.set(this.__rt_output.width, this.__rt_output.height);

        // remember renderer state
        const __old_state_rt = renderer.getRenderTarget();
        const __old_clear_color = scratch_color;
        renderer.getClearColor(__old_clear_color);
        const __old_clear_alpha = renderer.getClearAlpha();
        const __old_auto_clear = renderer.autoClear;

        //
        renderer.autoClear = false;
        renderer.setClearColor(this.__clear_color);
        renderer.setRenderTarget(this.__intermediate);
        renderer.clearColor();

        renderScreenSpace(renderer, mx);

        renderer.setRenderTarget(this.__rt_output);

        renderer.clearColor();

        renderScreenSpace(renderer, my);

        // restore state
        renderer.setRenderTarget(__old_state_rt);
        renderer.setClearColor(__old_clear_color, __old_clear_alpha);
        renderer.autoClear = __old_auto_clear;
    }

    /**
     *
     * @param {Texture} texture
     */
    setDepthBuffer(texture) {

        const mx = this.__material_blur_x;
        const my = this.__material_blur_y;

        mx.uniforms.tDepth.value = my.uniforms.tDepth.value = texture;
    }

    /**
     *
     * @param {WebGLRenderTarget|WebGLMultisampleRenderTarget} render_target
     */
    setInput(render_target) {
        this.__rt_input = render_target;

        // bind input
        this.__material_blur_x.uniforms.tDiffuse.value = render_target.texture;
    }

    /**
     *
     * @param {WebGLRenderTarget|WebGLMultisampleRenderTarget} render_target
     */
    setOutput(render_target) {
        this.__rt_output = render_target;

    }

    dispose() {
        this.__intermediate.dispose();
    }
}
