import { AdditiveBlending, Material, Mesh, MeshBasicMaterial, MeshStandardMaterial,ShadowMaterial } from "three";

import { ObjectUtils, PrimitiveType } from "../engine/engine_create_objects.js";
import { serializable } from "../engine/engine_serialization_decorator.js";
import { RGBAColor } from "../engine/js-extensions/index.js";
import { Behaviour } from "./Component.js";

/**
 * The mode of the ShadowCatcher.  
 * - ShadowMask: only renders shadows.
 * - Additive: renders shadows additively.
 * - Occluder: occludes light.
 */
enum ShadowMode {
    ShadowMask = 0,
    Additive = 1,
    Occluder = 2,
}

/**
 * ShadowCatcher can be added to an Object3D to make it render shadows (or light) in the scene. It can also be used to create a shadow mask, or to occlude light.    
 * If the GameObject is a Mesh, it will apply a shadow-catching material to it - otherwise it will create a quad with the shadow-catching material.  
 * 
 * Note that ShadowCatcher meshes are not raycastable by default; if you want them to be raycastable, change the layers in `onEnable()`.
 * @category Rendering
 * @group Components
 */
export class ShadowCatcher extends Behaviour {

    //@type Needle.Engine.ShadowCatcher.Mode
    @serializable()
    mode: ShadowMode = ShadowMode.ShadowMask;

    //@type UnityEngine.Color
    @serializable(RGBAColor)
    shadowColor: RGBAColor = new RGBAColor(0, 0, 0, 1);

    private targetMesh?: Mesh;

    /** @internal */
    start() {
        // if there's no geometry, make a basic quad
        if (!(this.gameObject instanceof Mesh)) {
            const quad = ObjectUtils.createPrimitive(PrimitiveType.Quad, {
                name: "ShadowCatcher",
                material: new MeshStandardMaterial({
                    // HACK heuristic to get approx. the same colors out as with the current default ShadowCatcher material
                    // not clear why this is needed; assumption is that the Renderer component does something we're not respecting here
                    color: 0x999999, 
                    roughness: 1,
                    metalness: 0,
                    transparent: true,
                })
            });
            quad.receiveShadow = true;
            quad.geometry.rotateX(-Math.PI / 2);

            // TODO breaks shadow catching right now
            // const renderer  = new Renderer();
            // renderer.receiveShadows = true;
            // GameObject.addComponent(quad, Renderer);
            
            this.gameObject.add(quad);
            this.targetMesh = quad;
        }
        else if (this.gameObject instanceof Mesh && this.gameObject.material) {
            // make sure we have a unique material to work with
            this.gameObject.material = this.gameObject.material.clone();
            this.targetMesh = this.gameObject;
            // make sure the mesh can receive shadows
            this.targetMesh.receiveShadow = true;
        }

        if(!this.targetMesh) {
            console.warn("ShadowCatcher: no mesh to apply shadow catching to. Groups are currently not supported.");
            return;
        }
        
        // Shadowcatcher mesh isnt raycastable
        this.targetMesh.layers.set(2);

        switch (this.mode) {
            case ShadowMode.ShadowMask:
                this.applyShadowMaterial();
                break;
            case ShadowMode.Additive:
                this.applyLightBlendMaterial();
                break;
            case ShadowMode.Occluder:
                this.applyOccluderMaterial();
                break;
        }
    }

    // Custom blending, diffuse-only lighting blended onto the scene additively.
    // Works great for Point Lights and spot lights, 
    // doesn't work for directional lights (since they're lighting up everything else).
    // Works even better with an additional black-ish gradient to darken parts of the AR scene
    // so that lights become more visible on bright surfaces.
    applyLightBlendMaterial() {
        if (!this.targetMesh) return;

        const material = this.targetMesh.material as Material;
        material.blending = AdditiveBlending;
        this.applyMaterialOptions(material);
        material.onBeforeCompile = (shader) => {
            // see https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderLib/meshphysical.glsl.js#L181
            // see https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderLib.js#LL284C11-L284C11
            // see https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderLib/shadow.glsl.js#L40
            // see https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderChunk/shadowmask_pars_fragment.glsl.js#L2
            // see https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderChunk/shadowmap_pars_fragment.glsl.js#L281

            shader.fragmentShader = shader.fragmentShader.replace("vec3 outgoingLight = totalDiffuse + totalSpecular + totalEmissiveRadiance;",
                `vec3 outgoingLight = totalDiffuse + totalSpecular + totalEmissiveRadiance;
            // diffuse-only lighting with overdrive to somewhat compensate
            // for the loss of indirect lighting and to make it more visible.
            vec3 direct = (reflectedLight.directDiffuse + reflectedLight.directSpecular) * 6.6;
            float max = max(direct.r, max(direct.g, direct.b));
            
            // early out - we're simply returning direct lighting and some alpha based on it so it can 
            // be blended onto the scene.
            gl_FragColor = vec4(direct, max);
            return;
            `);
        }
        material.userData.isLightBlendMaterial = true;
    }

    // ShadowMaterial: only does a mask; shadowed areas are fully black.
    // doesn't take light attenuation into account.
    // works great for Directional Lights.
    applyShadowMaterial() {
        if (this.targetMesh) {
            if ((this.targetMesh.material as Material).type !== "ShadowMaterial") {
                const material = new ShadowMaterial();
                material.color = this.shadowColor;
                material.opacity = this.shadowColor.alpha;
                this.applyMaterialOptions(material);
                this.targetMesh.material = material;
                material.userData.isShadowCatcherMaterial = true;
            }
            else {
                const material = this.targetMesh.material as ShadowMaterial;
                material.color = this.shadowColor;
                material.opacity = this.shadowColor.alpha;
                this.applyMaterialOptions(material);
                material.userData.isShadowCatcherMaterial = true;
            }
        }
    }

    applyOccluderMaterial() {
        if (this.targetMesh) {
            let material = this.targetMesh.material as Material;
            if (!material) {
                const mat = new MeshBasicMaterial();
                this.targetMesh.material = mat;
                material = mat;
            }
            material.depthWrite = true;
            material.stencilWrite = true;
            material.colorWrite = false;
            this.gameObject.renderOrder = -100;
        }
    }

    private applyMaterialOptions(material: Material) {
        if (material) {
            material.depthWrite = false;
            material.stencilWrite = false;
        }
    }
}