import { Object3D, Vector3 } from "three";
import { AnimationClip } from "three";
import { GLTFExporter, type GLTFExporterOptions } from 'three/examples/jsm/exporters/GLTFExporter.js';

import { SerializationContext } from "../../../engine/engine_serialization_core.js";
import { serializable } from "../../../engine/engine_serialization_decorator.js";
import { getWorldPosition } from "../../../engine/engine_three_utils.js";
import { getParam } from "../../../engine/engine_utils.js";
import GLTFMeshGPUInstancingExtension from '../../../engine/export/gltf/EXT_mesh_gpu_instancing_exporter.js';
import { RenderTextureWriter } from "../../../engine/export/gltf/Writers.js";
import { shouldExport_HideFlags } from "../../../engine/export/utils.js";
import { registerExportExtensions } from "../../../engine/extensions/index.js";
import { NEEDLE_components } from "../../../engine/extensions/NEEDLE_components.js";
import { BoxHelperComponent } from "../../BoxHelperComponent.js";
import { Behaviour, GameObject } from "../../Component.js";
import { Renderer } from "../../Renderer.js";

const debugExport = getParam("debuggltfexport");

declare type ExportOptions = GLTFExporterOptions & {
    pivot?: Vector3,
    needleComponents?: boolean,
}

export const componentsArrayExportKey = "$___Export_Components";

// @generate-component
export class GltfExportBox extends BoxHelperComponent {
    sceneRoot?: Object3D;
}

/**
 * GltfExport is a component that enables exporting selected 3D objects from the scene to the glTF format.  
 * You can specify whether to export in binary format (.glb) or JSON format (.gltf), and select specific objects to include in the export.
 * The exported glTF file can be used in various 3D applications and engines that support the glTF standard.
 * 
 * @summary Export selected 3D objects to glTF format
 * @category Asset Management
 * @group Components
 */
export class GltfExport extends Behaviour {

    @serializable()
    binary: boolean = true;

    @serializable(Object3D)
    objects: Object3D[] = [];

    private ext?: NEEDLE_components;

    async exportNow(name: string, opts?: ExportOptions) {

        if (debugExport) console.log("Exporting objects as glTF", this.objects);
        if (!name) name = "scene";
        if (!this.objects || this.objects.length <= 0)
            this.objects = [this.context.scene];

        const options = {
            binary: this.binary,
            pivot: GltfExport.calculateCenter(this.objects),
            ...opts
        };
        const res = await this.export(this.objects, options).catch(err => {
            console.error(err);
            return false;
        })
        if (res === false) return false;

        if (!this.binary) {
            if (!name.endsWith(".gltf"))
                name += ".gltf";
        }
        else if (!name.endsWith(".glb"))
            name += ".glb";
        if (this.binary)
            GltfExport.saveArrayBuffer(res, name);
        else
            GltfExport.saveJson(res, name);
        return true;
    }

    async export(objectsToExport: Object3D[], opts?: ExportOptions): Promise<any> {

        // -----------------------
        // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        // TODO: refactor this to use ../engine/export/index exportAsGLTF function

        // TODO add filtering / tags for what to export and what not < this is implemented in exportAsGLTF, see TODO above

        if (!objectsToExport || objectsToExport.length <= 0) {
            console.warn("No objects set to export");
            return;
        }

        // Instantiate a exporter
        const exporter = new GLTFExporter();
        exporter.register(writer => new GLTFMeshGPUInstancingExtension(writer));
        exporter.register(writer => new RenderTextureWriter(writer));
        registerExportExtensions(exporter, this.context);

        GltfExport.filterTopmostParent(objectsToExport);


        // https://threejs.org/docs/#examples/en/exporters/GLTFExporter
        const options = {
            trs: false,
            onlyVisible: true,
            truncateDrawRange: false,
            binary: true,
            maxTextureSize: Infinity, // To prevent NaN value,
            embedImages: true,
            includeCustomExtensions: true,
            animations: opts?.animations || GltfExport.collectAnimations(objectsToExport),
            ...opts
        };

        const undo = new Array<() => void>();

        const exportScene = new Object3D();
        // set the pivot position
        if (opts?.pivot) exportScene.position.sub(opts.pivot);
        // console.log(exportScene.position);

        // add objects for export
        if (debugExport) console.log("EXPORT", objectsToExport);
        objectsToExport.forEach(obj => {
            if (obj && shouldExport_HideFlags(obj)) {
                // adding directly does not require us to change parents and mess with the hierarchy actually
                exportScene.children.push(obj);
                // TODO: we should probably be doing this before writing nodes?? apply world scale, position, rotation etc for export only
                obj.matrixAutoUpdate = false;
                obj.matrix.copy(obj.matrixWorld);
                // disable instancing
                GameObject.getComponentsInChildren(obj, Renderer).forEach(r => {
                    if (GameObject.isActiveInHierarchy(r.gameObject)) r.setInstancingEnabled(false)
                });
                obj.traverse(o => {
                    if (!shouldExport_HideFlags(o)) {
                        const parent = o.parent;
                        o.removeFromParent();
                        undo.push(() => {
                            if (parent) parent.add(o);
                        });
                    }
                })
            }
        });

        const serializationContext = new SerializationContext(exportScene);

        if (opts?.needleComponents) {
            this.ext = new NEEDLE_components();
        }
        if (this.ext) {
            this.ext.registerExport(exporter);
            this.ext.context = serializationContext;
        }

        return new Promise((resolve, reject) => {
            if (debugExport) console.log("Starting glTF export.")
            try {
                // Parse the input and generate the glTF output
                exporter?.parse(
                    exportScene,
                    // called when the gltf has been generated
                    res => {
                        cleanup();
                        resolve(res);
                    },
                    // called when there is an error in the generation
                    err => {
                        cleanup();
                        reject(err);
                    },
                    options
                );
            }
            catch (err) {
                console.error(err);
                reject(err);
            }
            finally {
                undo.forEach(u => u());
                if (debugExport) console.log("Finished glTF export.");
            }
        });

        function cleanup() {
            objectsToExport.forEach(obj => {
                if (!obj) return;
                obj.matrixAutoUpdate = true;
                GameObject.getComponentsInChildren(obj, Renderer).forEach(r => {
                    if (GameObject.isActiveInHierarchy(r.gameObject)) r.setInstancingEnabled(false)
                });
            });
        }
    };

    private static saveArrayBuffer(buffer, filename) {
        this.save(new Blob([buffer], { type: 'application/octet-stream' }), filename);
    }

    private static saveJson(json, filename) {
        this.save("data: text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(json)), filename);
    }

    private static save(blob, filename) {
        const link = document.createElement('a');
        link.style.display = 'none';
        document.body.appendChild(link); // Firefox workaround, see #6594
        if (typeof blob === "string")
            link.href = blob;
        else
            link.href = URL.createObjectURL(blob);
        link.download = filename;
        link.click();
        link.remove();
        // console.log(link.href);
        // URL.revokeObjectURL( url ); breaks Firefox...
    }

    private static collectAnimations(objs: Object3D[], target?: Array<AnimationClip>): Array<AnimationClip> {
        target = target || [];
        for (const obj of objs) {
            if (!obj) continue;
            obj.traverseVisible(o => {
                if (o.animations && o.animations.length > 0)
                    target!.push(...o.animations);
            });
        }
        return target;
    }


    private static calculateCenter(objs: Object3D[], target?: Vector3): Vector3 {
        const center = target || new Vector3();
        center.set(0, 0, 0);
        objs.forEach(obj => {
            center.add(getWorldPosition(obj));
        });
        center.divideScalar(objs.length);
        return center;
    }

    private static filterTopmostParent(objs: Object3D[]) {
        if (objs.length <= 0) return;
        for (let index = 0; index < objs.length; index++) {
            let obj = objs[index];
            if (!obj) {
                objs.splice(index, 1);
                index--;
                continue;
            }
            // loop hierarchy up and kick object if any of its parents is already in this list
            // because then this object will already be exported (and we dont want to export it)
            while (obj.parent) {
                if (objs.includes(obj.parent)) {
                    // console.log("FILTER", objs[index]);
                    objs.splice(index, 1);
                    index--;
                    break;
                }
                obj = obj.parent;
            }
        }
    }

}