import { createLoaders } from "@needle-tools/gltf-progressive";
import { CompressedCubeTexture, CubeRefractionMapping, CubeTexture, EquirectangularRefractionMapping, SRGBColorSpace, Texture, TextureLoader } from "three"
import { EXRLoader } from "three/examples/jsm/loaders/EXRLoader.js";
import { KTX2Loader } from "three/examples/jsm/loaders/KTX2Loader.js";
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js';

import { disposeObjectResources, setDisposable } from "../engine/engine_assetdatabase.js";
import { ContextEvent, ContextRegistry } from "../engine/engine_context_registry.js";
import { registerObservableAttribute } from "../engine/engine_element_extras.js";
import { syncField } from "../engine/engine_networking_auto.js";
import { serializable } from "../engine/engine_serialization_decorator.js";
import { type IContext } from "../engine/engine_types.js";
import { addAttributeChangeCallback, getParam, PromiseAllWithErrors, removeAttributeChangeCallback } from "../engine/engine_utils.js";
import { Camera, ClearFlags } from "./Camera.js";
import { Behaviour, GameObject } from "./Component.js";

const debug = getParam("debugskybox");

registerObservableAttribute("skybox-image");
registerObservableAttribute("environment-image");

function createRemoteSkyboxComponent(context: IContext, url: string, skybox: boolean, environment: boolean, attribute: "skybox-image" | "environment-image") {
    const remote = new RemoteSkybox();
    remote.allowDrop = false;
    remote.allowNetworking = false;
    remote.background = skybox;
    remote.environment = environment;
    GameObject.addComponent(context.scene, remote);
    const urlChanged = newValue => {
        if (typeof newValue !== "string") return;
        if (debug) console.log(attribute, "CHANGED TO", newValue)
        remote.setSkybox(newValue);
    };
    addAttributeChangeCallback(context.domElement, attribute, urlChanged);
    remote.addEventListener("destroy", () => {
        if (debug) console.log("Destroyed attribute remote skybox", attribute);
        removeAttributeChangeCallback(context.domElement, attribute, urlChanged);
    });
    return remote.setSkybox(url);
}

const promises = new Array<Promise<any>>();
ContextRegistry.registerCallback(ContextEvent.ContextCreationStart, (args) => {
    const context = args.context;
    const skyboxImage = context.domElement.getAttribute("skybox-image") || context.domElement.getAttribute("background-image");
    const environmentImage = context.domElement.getAttribute("environment-image");
    if (skyboxImage) {
        if (debug)
            console.log("Creating remote skybox to load " + skyboxImage);
        // if the user is loading a GLB without a camera then the CameraUtils (which creates the default camera)
        // checks if we have this attribute set and then sets the skybox clearflags accordingly
        // if the user has a GLB with a camera but set to solid color then the skybox image is not visible -> we will just warn then and not override the camera settings
        if (context.mainCameraComponent?.clearFlags !== ClearFlags.Skybox) console.warn("\"skybox-image\"/\"background-image\" attribute has no effect: camera clearflags are not set to \"Skybox\"");
        const promise = createRemoteSkyboxComponent(context, skyboxImage, true, false, "skybox-image");
        promises.push(promise);
    }
    if (environmentImage) {
        if (debug)
            console.log("Creating remote environment to load " + environmentImage);
        const promise = createRemoteSkyboxComponent(context, environmentImage, false, true, "environment-image");
        promises.push(promise);
    }
});
ContextRegistry.registerCallback(ContextEvent.ContextCreationStart, () => {
    return Promise.all(promises).finally(() => {
        promises.length = 0;
    })
});

declare type SkyboxCacheEntry = { src: string, texture: Promise<Texture> };
function ensureGlobalCache() {
    if (!globalThis["NEEDLE_ENGINE_SKYBOX_TEXTURES"])
        globalThis["NEEDLE_ENGINE_SKYBOX_TEXTURES"] = new Array<SkyboxCacheEntry>();
    return globalThis["NEEDLE_ENGINE_SKYBOX_TEXTURES"] as Array<SkyboxCacheEntry>;
}

function tryGetPreviouslyLoadedTexture(src: string) {
    const cache = ensureGlobalCache();
    const found = cache.find(x => x.src === src);
    if (found) {
        if (debug) console.log("Skybox: Found previously loaded texture for " + src);
        return found.texture;
    }
    return null;
}
async function disposeCachedTexture(tex: Promise<Texture>) {
    const texture = await tex;
    setDisposable(texture, true);
    disposeObjectResources(texture);
}
function registerLoadedTexture(src: string, texture: Promise<Texture>) {
    const cache = ensureGlobalCache();
    // Make sure the cache doesnt get too big
    while (cache.length > 5) {
        const entry = cache.shift();
        if (entry) { disposeCachedTexture(entry.texture); }
    }
    texture.then(t => { return setDisposable(t, false) });
    cache.push({ src, texture });
}


/**
 * RemoteSkybox is a component that allows you to set the skybox of a scene from a URL or a local file.  
 * It supports .hdr, .exr, .jpg, .png files.
 * 
 * ### Events
 * - `dropped-unknown-url`: Emitted when a file is dropped on the scene. The event detail contains the sender, the url and a function to apply the url.
 * 
 * @example adding a skybox
 * ```ts
 * GameObject.addComponent(gameObject, Skybox, { url: "https://example.com/skybox.hdr", background: true, environment: true });
 * ```
 * 
 * @example handle custom url
 * ```ts
 * const skybox = GameObject.addComponent(gameObject, Skybox);
 * skybox.addEventListener("dropped-unknown-url", (evt) => {
 *    let url = evt.detail.url;
 *    console.log("User dropped file", url);
 *    // change url or resolve it differently
 *    url = "https://example.com/skybox.hdr";
 *    // apply the url
 *    evt.detail.apply(url);
 * });
 * ```
 */
export class RemoteSkybox extends Behaviour {

    /**
     * URL to a remote skybox. This value can also use a magic skybox name. Options are "quicklook", "quicklook-ar", "studio", "blurred-skybox".
     * @example
     * ```ts
     * skybox.url = "https://example.com/skybox.hdr";
     * ```
     */
    @syncField(RemoteSkybox.prototype.urlChangedSyncField)
    @serializable(URL)
    url?: string;

    /**
     * When enabled a user can drop a link to a skybox image on the scene to set the skybox.
     * @default true
     */
    @serializable()
    allowDrop: boolean = true;

    /**
     * When enabled the skybox will be set as the background of the scene.
     * @default true
     */
    @serializable()
    background: boolean = true;

    /**
     * When enabled the skybox will be set as the environment of the scene (to be used as environment map for reflections and lighting)
     * @default true
     */
    @serializable()
    environment: boolean = true;

    /**
     * When enabled dropped skybox urls (or assigned skybox urls) will be networked to other users in the same networked room.
     * @default true
     */
    @serializable()
    allowNetworking: boolean = true;

    private _loader?: RGBELoader | EXRLoader | TextureLoader | KTX2Loader;
    private _prevUrl?: string;
    private _prevLoadedEnvironment?: Texture;
    private _prevEnvironment: Texture | null = null;
    private _prevBackground: any = null;

    /** @internal */
    onEnable() {
        this.setSkybox(this.url);
        this.registerDropEvents();
    }

    /** @internal */
    onDisable() {
        if (this.context.scene.environment === this._prevLoadedEnvironment) {
            this.context.scene.environment = this._prevEnvironment;
            if (!Camera.backgroundShouldBeTransparent(this.context))
                this.context.scene.background = this._prevBackground;
            this._prevLoadedEnvironment = undefined;
        }
        this.unregisterDropEvents();
        // Re-apply the skybox/background settings of the main camera
        this.context.mainCameraComponent?.applyClearFlags();
    }

    private urlChangedSyncField() {
        if (this.allowNetworking && this.url) {
            // omit local dragged files from being handled
            if (this.isRemoteTexture(this.url)) {
                this.setSkybox(this.url);
            }
        }
    }

    /**
     * Set the skybox from a given url
     * @param url The url of the skybox image
     * @param name Define name of the file with extension if it isn't apart of the url
     * @returns Whether the skybox was successfully set
     */
    async setSkybox(url: string | undefined | null, name?: string) {
        if (!this.activeAndEnabled) return false;

        url = tryParseMagicSkyboxName(url, this.environment, this.background);

        if (!url) return false;

        name ??= url;

        if (!this.isValidTextureType(name)) {
            console.warn("Potentially invalid skybox url", name, "on", this.name);
        }

        if (debug) console.log("Set remote skybox url: " + url);

        if (this._prevUrl === url && this._prevLoadedEnvironment) {
            this.applySkybox();
            return true;
        }
        else {
            this._prevLoadedEnvironment?.dispose();
            this._prevLoadedEnvironment = undefined;
        }
        this._prevUrl = url;

        const envMap = await this.loadTexture(url, name);
        if (!envMap) return false;
        // Check if we're still enabled
        if (!this.enabled) return false;
        // Update the current url
        this.url = url;
        const nameIndex = url.lastIndexOf("/");
        envMap.name = url.substring(nameIndex >= 0 ? nameIndex + 1 : 0);
        if (this._loader instanceof TextureLoader) {
            envMap.colorSpace = SRGBColorSpace;
        }
        this._prevLoadedEnvironment = envMap;
        this.applySkybox();
        return true;
    }

    private async loadTexture(url: string, name?: string) {
        if (!url) return Promise.resolve(null);
        name ??= url;
        const cached = tryGetPreviouslyLoadedTexture(name);
        if (cached) {
            const res = await cached;
            if (res.source?.data?.length > 0 || res.source?.data?.data?.length) return res;
        }
        const isEXR = name.endsWith(".exr");
        const isHdr = name.endsWith(".hdr");
        const isKtx2 = name.endsWith(".ktx2");
        if (isEXR) {
            if (!(this._loader instanceof EXRLoader))
                this._loader = new EXRLoader();
        }
        else if (isHdr) {
            if (!(this._loader instanceof RGBELoader))
                this._loader = new RGBELoader();
        }
        else if (isKtx2) {
            if (!(this._loader instanceof KTX2Loader)) {
                const { ktx2Loader } = createLoaders(this.context.renderer);
                this._loader = ktx2Loader;
            }
        }
        else {
            if (!(this._loader instanceof TextureLoader))
                this._loader = new TextureLoader();
        }

        if (debug) console.log("Loading skybox: " + url);
        const loadingTask = this._loader.loadAsync(url);
        registerLoadedTexture(name, loadingTask);
        const envMap = await loadingTask;
        return envMap;
    }

    private applySkybox() {
        const envMap = this._prevLoadedEnvironment;
        if (!envMap) return;


        if ((envMap instanceof CubeTexture || envMap instanceof CompressedCubeTexture)) {
            // Nothing to do
        }
        else {
            envMap.mapping = EquirectangularRefractionMapping;
            envMap.needsUpdate = true;
        }


        // capture state
        if (this.context.scene.background !== envMap)
            this._prevBackground = this.context.scene.background;
        if (this.context.scene.environment !== envMap)
            this._prevEnvironment = this.context.scene.environment;
        if (debug) console.log("Set remote skybox", this.url, !Camera.backgroundShouldBeTransparent(this.context));
        if (this.environment)
            this.context.scene.environment = envMap;
        if (this.background && !Camera.backgroundShouldBeTransparent(this.context))
            this.context.scene.background = envMap;
        if (this.context.mainCameraComponent?.backgroundBlurriness !== undefined)
            this.context.scene.backgroundBlurriness = this.context.mainCameraComponent.backgroundBlurriness;
    }


    private readonly validTextureTypes = [".ktx2", ".hdr", ".exr", ".jpg", ".jpeg", ".png"];

    private isRemoteTexture(url: string): boolean {
        return url.startsWith("http://") || url.startsWith("https://");
    }

    private isValidTextureType(url: string): boolean {
        for (const type of this.validTextureTypes) {
            if (url.endsWith(type)) return true;
        }
        return false;
    }



    private registerDropEvents() {
        this.unregisterDropEvents();
        this.context.domElement.addEventListener("dragover", this.onDragOverEvent);
        this.context.domElement.addEventListener("drop", this.onDrop);
    }

    private unregisterDropEvents() {
        this.context.domElement.removeEventListener("dragover", this.onDragOverEvent);
        this.context.domElement.removeEventListener("drop", this.onDrop);
    }

    private onDragOverEvent = (e: DragEvent) => {
        if (!this.allowDrop) return;
        if (!e.dataTransfer) return;
        for (const type of e.dataTransfer.types) {
            // in ondragover we dont get access to the content
            // but if we have a uri list we can assume
            // someone is maybe dragging a image file
            // so we want to capture this
            if (type === "text/uri-list" || type === "Files") {
                e.preventDefault();
            }
        }
    };

    private onDrop = (e: DragEvent) => {
        if (!this.allowDrop) return;
        if (!e.dataTransfer) return;
        for (const type of e.dataTransfer.types) {
            if (debug) console.log(type);
            if (type === "text/uri-list") {
                const url = e.dataTransfer.getData(type);
                if (debug) console.log(type, url);
                let name = new RegExp(/polyhaven.com\/asset_img\/.+?\/(?<name>.+)\.png/).exec(url)?.groups?.name;
                if (!name) {
                    name = new RegExp(/polyhaven\.com\/a\/(?<name>.+)/).exec(url)?.groups?.name;
                }
                if (debug) console.log(name);
                if (name) {
                    const skyboxurl = "https://dl.polyhaven.org/file/ph-assets/HDRIs/exr/1k/" + name + "_1k.exr";
                    console.log(`[Remote Skybox] Setting skybox from url: ${skyboxurl}`);
                    e.preventDefault();
                    this.setSkybox(skyboxurl);
                    break;
                }
                else if (this.isValidTextureType(url)) {
                    console.log("[Remote Skybox] Setting skybox from url: " + url);
                    e.preventDefault();
                    this.setSkybox(url);
                    break;
                }
                else {

                    console.warn(`[RemoteSkybox] Unknown url ${url}. If you want to load a skybox from a url, make sure it is a valid image url. Url must end with${this.validTextureTypes.join(", ")}.`);
                    // emit custom event - users can listen to this event and handle the url themselves
                    const evt = new CustomEvent("dropped-unknown-url", {
                        detail: {
                            sender: this,
                            event: e,
                            url,
                            apply: (url: string) => {
                                e.preventDefault();
                                this.setSkybox(url);
                            }
                        }
                    });
                    this.dispatchEvent(evt);
                }
            }
            else if (type == "Files") {
                const file = e.dataTransfer.files.item(0);
                if (debug) console.log(type, file);
                if (!file) continue;
                if (!this.isValidTextureType(file.name)) {
                    console.warn(`[RemoteSkybox]: File \"${file.name}\" is not supported. Supported files are ${this.validTextureTypes.join(", ")}`);
                    return;
                }
                if (tryGetPreviouslyLoadedTexture(file.name) === null) {
                    const blob = new Blob([file]);
                    const url = URL.createObjectURL(blob);
                    e.preventDefault();
                    this.setSkybox(url, file.name);
                }
                else {
                    e.preventDefault();
                    this.setSkybox(file.name);
                }
                break;
            }
        }
    };
}




function tryParseMagicSkyboxName(str: string | null | undefined, environment: boolean, background: boolean): string | null {

    const useLowRes = environment && !background;

    switch (str?.toLowerCase()) {
        case "studio":
            if (useLowRes) {
                return "https://cdn.needle.tools/static/skybox/modelviewer-Neutral-small.hdr";
            }
            else return "https://cdn.needle.tools/static/skybox/modelviewer-Neutral.hdr";

        case "blurred-skybox":
            if (useLowRes) {
                return "https://cdn.needle.tools/static/skybox/blurred-skybox-small.exr";
            }
            return "https://cdn.needle.tools/static/skybox/blurred-skybox.exr";
        case "quicklook-ar":
            if (useLowRes) {
                return "https://cdn.needle.tools/static/skybox/QuickLook-ARMode-small.exr";
            }
            return "https://cdn.needle.tools/static/skybox/QuickLook-ARMode.exr";
        case "quicklook":
            if (useLowRes) {
                return "https://cdn.needle.tools/static/skybox/QuickLook-ObjectMode-small.exr";
            }
            return "https://cdn.needle.tools/static/skybox/QuickLook-ObjectMode.exr";
    }
    if (str === undefined) return null;
    return str;
}