import {
    AddEquation,
    CustomBlending,
    Mesh,
    OneFactor,
    OneMinusSrcAlphaFactor,
    RedFormat,
    RGBAFormat,
    ShaderMaterial
} from "three";
import { assert } from "../../../core/assert.js";
import Signal from "../../../core/events/signal/Signal.js";
import Vector2 from "../../../core/geom/Vector2.js";
import { FULL_SCREEN_TRIANGLE_GEOMETRY } from "../geometry/FULL_SCREEN_TRIANGLE_GEOMETRY.js";
import { ScreenSpaceQuadShader } from "../shaders/ScreenSpaceQuadShader.js";
import { BlendingType } from "../texture/sampler/BlendingType.js";
import { blendingType2three } from "../texture/sampler/blendingType2Three.js";

/**
 *
 * @param {Material} material
 * @param {BlendingType} blending
 */
function assignMaterialBlending(material, blending) {

    if (blending === BlendingType.MultiplyAdd) {
        material.blending = CustomBlending;
        material.blendEquation = AddEquation;
        material.blendSrc = OneFactor;
        material.blendDst = OneMinusSrcAlphaFactor;
    } else {
        const threeBlending = blendingType2three(blending);

        material.blending = threeBlending;
    }

}

export class CompositLayer {
    /**
     *
     * @param {THREE.WebGLRenderTarget} renderTarget
     * @param {BlendingType} blending
     * @param {number} renderTargetScale
     */
    constructor({
                    renderTarget,
                    blending,
                    renderTargetScale = 1
                }) {
        /**
         * Human-readable name, useful for debugging
         * @type {string}
         */
        this.name = "";

        /**
         *
         * @type {BlendingType}
         */
        this.__blending = blending;
        /**
         *
         * @type {WebGLRenderTarget}
         */
        this.renderTarget = renderTarget;

        /**
         *
         * @type {number}
         */
        this.renderTargetScale = renderTargetScale;

        this.object = new Mesh(FULL_SCREEN_TRIANGLE_GEOMETRY, this.buildMaterial());

        /**
         * Which compositing stage this layer belongs to, stages are rendered independently
         * @type {number}
         */
        this.stage = 0;

        this.size = new Vector2();

        /**
         *
         * @type {boolean}
         */
        this.__enabled = true;

        /**
         * Layers are composited in order from lowest to highest
         * @type {number}
         */
        this.__order = 0;

        this.on = {
            preRender: new Signal()
        };
    }

    get blending() {
        return this.__blending;
    }

    set blending(v) {
        if (v === this.__blending) {
            return;
        }

        this.__blending = v;

        assignMaterialBlending(this.object.material, v);
    }

    /**
     *
     * @param {number} v
     */
    set order(v) {
        this.__order = v;

        this.object.renderOrder = v;
    }

    /**
     *
     * @return {number}
     */
    get order() {
        return this.__order;
    }

    /**
     *
     * @param {number} v
     */
    set enabled(v) {
        if (!this.__enabled && v) {
            this.enable();
        } else if (this.__enabled && !v) {
            this.disable();
        }
    }

    /**
     *
     * @returns {boolean}
     */
    get enabled() {
        return this.__enabled;
    }

    enable() {
        if (!this.__enabled) {
            this.__enabled = true;
            this.object.visible = true;
        }
    }

    disable() {
        if (this.__enabled) {
            this.__enabled = false;
            this.object.visible = false;
        }
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     */
    setSize(x, y) {
        this.size.set(x, y);
        this.updateRenderTargetSize();
    }

    updateRenderTargetSize() {

        if (this.renderTarget !== null) {
            const scale = this.renderTargetScale;

            const size = this.size;

            // clamp to 1: a 0-sized attachment trips GL_INVALID_FRAMEBUFFER_OPERATION on clear/draw
            const width = Math.max(1, size.x * scale);
            const height = Math.max(1, size.y * scale);

            this.renderTarget.setSize(width, height);
        }
    }

    /**
     *
     * @param {number} scale
     */
    setRenderTargetScale(scale) {
        assert.isNumber(scale, 'v');
        assert.isFinite(scale, 'v');

        this.renderTargetScale = scale;

        this.updateRenderTargetSize();
    }

    /**
     * Override screen-space material of the layer. This allows you to render directly into the output buffer, bypassing the need to create an intermediate buffer
     * NOTE: Please make sure you know what you're doing if you use this method
     * @param {Material} material
     */
    setCustomMaterial(material) {
        assignMaterialBlending(material, this.blending);

        // override material
        this.object.material = material;
    }


    buildMaterial() {


        const uniforms = {
            tTexture: {
                type: 't',
                value: this.renderTarget.texture
            }
        };

        // determine swizzle for the material
        let swizzle;
        const texture_format = this.renderTarget.texture.format;
        if (texture_format === RedFormat) {
            swizzle = ['r', 'r', 'r', 1];
        } else if (texture_format === RGBAFormat) {
            swizzle = ['r', 'g', 'b', 'a'];
        } else {
            throw new Error('Unsupported format');
        }

        const material = new ShaderMaterial({
            uniforms,
            vertexShader: ScreenSpaceQuadShader.vertexShader(),
            fragmentShader: ScreenSpaceQuadShader.fragmentShader(swizzle),
            lights: false,
            fog: false,
            depthTest: false,
            depthWrite: false,
            transparent: true,
            vertexColors: false
        });

        assignMaterialBlending(material, this.blending);

        return material;
    }
}
