import { EquirectangularReflectionMapping, LinearSRGBColorSpace, Material, MeshBasicMaterial, Object3D, SRGBColorSpace, Texture, Vector3 } from "three";

import { isDevEnvironment, showBalloonWarning } from "../engine/debug/index.js";
import { serializable } from "../engine/engine_serialization.js";
import { Context } from "../engine/engine_setup.js";
import type { IRenderer } from "../engine/engine_types.js";
import { getParam } from "../engine/engine_utils.js";
import { BoxHelperComponent } from "./BoxHelperComponent.js";
import { Behaviour } from "./Component.js";

export const debug = getParam("debugreflectionprobe");
const disable = getParam("noreflectionprobe");

const $reflectionProbeKey = Symbol("reflectionProbeKey");
const $originalMaterial = Symbol("original material");

/**
 * @category Rendering
 * @group Components
 */
export class ReflectionProbe extends Behaviour {

    private static _probes: Map<Context, ReflectionProbe[]> = new Map();

    public static get(object: Object3D | null | undefined, context: Context, isAnchor: boolean, anchor?: Object3D): ReflectionProbe | null {
        if (!object || object.isObject3D !== true) return null;
        if (disable) return null;
        const probes = ReflectionProbe._probes.get(context);
        if (probes) {
            for (const probe of probes) {
                if (!probe.__didAwake) probe.__internalAwake();
                if (probe.enabled) {
                    if (anchor) {
                        // test if anchor is reflection probe object
                        if (probe.gameObject === anchor) {
                            return probe;
                        }
                    }
                    // TODO not supported right now, as we'd have to pass the ReflectionProbe scale through as well.
                    else if (probe.isInBox(object)) {
                        if (debug) console.log("Found reflection probe", object.name, probe.name);
                        return probe;
                    }
                }
            }
        }
        if (debug)
            console.debug("Did not find reflection probe", object.name, isAnchor, object);
        return null;
    }



    private _texture!: Texture;

    // @serializable(Texture)
    set texture(tex: Texture) {
        if (tex && !(tex instanceof Texture)) {
            console.error("ReflectionProbe.texture must be a Texture", tex);
            return;
        }
        this._texture = tex;
        if (tex) {
            tex.mapping = EquirectangularReflectionMapping;
            tex.colorSpace = LinearSRGBColorSpace;
            tex.needsUpdate = true;
        }
    }
    get texture(): Texture {
        return this._texture;
    }

    @serializable(Vector3)
    center?: Vector3;
    @serializable(Vector3)
    size?: Vector3;

    private _boxHelper?: BoxHelperComponent;

    private isInBox(obj: Object3D) {
        return this._boxHelper?.isInBox(obj);
    }

    constructor() {
        super();
        if (!ReflectionProbe._probes.has(this.context)) {
            ReflectionProbe._probes.set(this.context, []);
        }
        ReflectionProbe._probes.get(this.context)?.push(this);
    }

    awake() {
        this._boxHelper = this.gameObject.addComponent(BoxHelperComponent) as BoxHelperComponent;
        this._boxHelper.updateBox(true);
        if (debug)
            this._boxHelper.showHelper(0x555500, true);

        if (this._texture) {
            this._texture.mapping = EquirectangularReflectionMapping;
            this._texture.colorSpace = LinearSRGBColorSpace;
            this._texture.needsUpdate = true;
        }
    }
    start(): void {
        if (!this._texture && isDevEnvironment()) {
            console.warn(`[ReflectionProbe] Missing texture. Please assign a custom cubemap texture. To use reflection probes assign them to your renderer's "anchor" property.`);
            showBalloonWarning("ReflectionProbe configuration hint: See browser console for details")
        }
    }

    onDestroy() {
        const probes = ReflectionProbe._probes.get(this.context);
        if (probes) {
            const index = probes.indexOf(this);
            if (index >= 0) {
                probes.splice(index, 1);
            }
        }
    }


    // when objects are rendered and they share material
    // and some need reflection probe and some don't
    // we need to make sure we don't override the material but use a copy

    private static _rendererMaterialsCache: Map<IRenderer, Array<{ material: Material, copy: Material }>> = new Map();

    onSet(_rend: IRenderer) {
        if (disable) return;
        if (!this.enabled) return;
        if (_rend.sharedMaterials?.length <= 0) return;
        if (!this.texture) return;

        let rendererCache = ReflectionProbe._rendererMaterialsCache.get(_rend);
        if (!rendererCache) {
            rendererCache = [];
            ReflectionProbe._rendererMaterialsCache.set(_rend, rendererCache);
        }

        // TODO: dont clone material for every renderer that uses reflection probes, we can do it once per material when they use the same reflection texture

        // need to make sure materials are not shared when using reflection probes
        // otherwise some renderers outside of the probe will be affected or vice versa
        for (let i = 0; i < _rend.sharedMaterials.length; i++) {
            const material = _rend.sharedMaterials[i];
            if (!material) {
                continue;
            }
            if (material["envMap"] === undefined) {
                continue;
            }

            if (material instanceof MeshBasicMaterial) {
                continue;
            }

            let cached = rendererCache[i];

            // make sure we have the currently assigned material cached (and an up to date clone of that)
            // TODO: this is causing problems with progressive textures sometimes (depending on the order) when the version changes and we re-create materials over and over. We might want to just set the material envmap instead of making a clone
            const isCachedInstance = material === cached?.copy;
            const hasChanged = !cached || cached.material.uuid !== material.uuid || cached.copy.version !== material.version;
            if (!isCachedInstance && hasChanged) {
                if (debug) {
                    let reason = "";
                    if (!cached) reason = "not cached";
                    else if (cached.material !== material) reason = "reference changed; cached instance?: " + isCachedInstance;
                    else if (cached.copy.version !== material.version) reason = "version changed";
                    console.warn("Cloning material", material.name, material.version, "Reason:", reason, "\n", material.uuid, "\n", cached?.copy.uuid, "\n", _rend.name);
                }

                const clone = material.clone();
                clone.version = material.version;

                if (cached) {
                    cached.copy = clone;
                    cached.material = material;
                }
                else {
                    cached = {
                        material: material,
                        copy: clone
                    };
                    rendererCache.push(cached);
                }

                clone[$reflectionProbeKey] = this;
                clone[$originalMaterial] = material;

                if (debug) console.log("Set reflection", _rend.name, _rend.guid);
            }

            // See NE-4771 and NE-4856
            if (cached && cached.copy) {
                cached.copy.onBeforeCompile = material.onBeforeCompile;
            }

            /** this is the material that we copied and that has the reflection probe */
            const copy = cached?.copy;

            // make sure the reflection probe is assigned
            copy["envMap"] = this.texture;

            _rend.sharedMaterials[i] = copy;
        }
    }

    onUnset(_rend: IRenderer) {
        const rendererCache = ReflectionProbe._rendererMaterialsCache.get(_rend);
        if (rendererCache) {
            for (let i = 0; i < rendererCache.length; i++) {
                const cached = rendererCache[i];
                _rend.sharedMaterials[i] = cached.material;
            }
        }
    }
}