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";
import type { ContactShadows } from "./ContactShadows.js";
import type { Light } from "./Light.js";
import type { Renderer } from "./Renderer.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 renders real-time shadows cast by lights onto a mesh surface.  
 * Captures actual shadow data from the scene's lighting system (directional lights, point lights, spot lights).  
 *
 * If the GameObject is a Mesh, it applies a shadow-catching material to it.  
 * Otherwise, it creates a quad mesh with the shadow-catching material automatically.  
 *
 * [![](https://cloud.needle.tools/-/media/pFXPchA4vynNKOjgG_KucQ.gif)](https://engine.needle.tools/samples/shadow-catcher/)   
 * *Additive ShadowCatcher mode with point light shadows*  
 *
 * [![](https://cloud.needle.tools/-/media/oIWgEU49rEA0xJ2TrbzVlg.gif)](https://engine.needle.tools/samples/transmission/)   
 * *ShadowCatcher with directional light shadows*
 *
 * **Shadow Modes:**  
 * - `ShadowMask` - Only renders shadows (works best with directional lights)  
 * - `Additive` - Renders light additively (works best with point/spot lights)
 * - `Occluder` - Occludes light without rendering shadows
 *
 * **ShadowCatcher vs ContactShadows:**
 * - **ShadowCatcher**: Real-time shadows from actual lights. Accurate directional shadows that match light sources. Requires lights with shadows enabled. Updates every frame.
 * - **{@link ContactShadows}**: Proximity-based ambient occlusion-style shadows. Extremely soft and diffuse, ideal for subtle grounding. Better performance, works without lights.
 *
 * **When to use ShadowCatcher:**
 * - You need accurate shadows that match specific light directions
 * - Scene has real-time lighting with shadow-casting lights
 * - Shadows need to follow light attenuation and angles
 * - AR/VR scenarios where light estimation is available
 * - Hard or semi-hard shadow edges are desired
 *
 * **When to use ContactShadows instead:**
 * - You want very soft, ambient occlusion-style ground shadows
 * - Performance is critical (no per-frame shadow rendering)
 * - Scene doesn't have shadow-casting lights
 * - Product visualization or configurators (subtle grounding effect)
 * - Soft, diffuse shadows are more visually appealing than accurate ones
 *
 * **Note:** ShadowCatcher meshes are not raycastable by default (layer 2). Change layers in `onEnable()` if raycasting is needed.
 *
 * @example Basic shadow catcher plane
 * ```ts
 * const plane = new Object3D();
 * const catcher = addComponent(plane, ShadowCatcher);
 * catcher.mode = ShadowMode.ShadowMask;
 * catcher.shadowColor = new RGBAColor(0, 0, 0, 0.8);
 * ```
 *
 * @example Apply to existing mesh
 * ```ts
 * const mesh = this.gameObject.getComponent(Mesh);
 * const catcher = addComponent(mesh, ShadowCatcher);
 * // The mesh will now catch shadows from scene lights
 * ```
 *
 * @summary Renders real-time shadows from lights onto surfaces
 * @category Rendering
 * @group Components
 * @see {@link ContactShadows} for proximity-based fake shadows (better performance)
 * @see {@link Light} for shadow-casting light configuration
 * @see {@link Renderer} for shadow receiving settings
 * @link https://engine.needle.tools/samples/shadow-catcher/
 * @link https://engine.needle.tools/samples/transmission/
 */
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;
        }
    }
}