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

import { AnimationUtils } from "../../engine_animation.js";
import type { Context } from "../../engine_setup.js";
import { registerExportExtensions } from "../../extensions/index.js";
import { __isExporting } from "../state.js";
import { shouldExport_HideFlags } from "../utils.js";
import GLTFMeshGPUInstancingExtension from "./EXT_mesh_gpu_instancing_exporter.js";
import { GizmoWriter as GLTFGizmoWriter, RenderTextureWriter as GLTFRenderTextureWriter } from "./Writers.js";

declare type ExportOptions = {
    context: Context,
    scene?: Object3D | Array<Object3D>,
    binary?: boolean,
    animations?: boolean,
    downloadAs?: string,
}

const DEFAULT_OPTIONS: Omit<ExportOptions, "context" | "scene"> = {
    binary: true,
    animations: true,
}

export async function exportAsGLTF(_opts: ExportOptions): Promise<ArrayBuffer | Record<string, any>> {

    if (!_opts.context) {
        throw new Error("No context provided to exportAsGLTF");
    }

    if (!_opts.scene) {
        _opts.scene = _opts.context.scene;
    }

    const opts = {
        ...DEFAULT_OPTIONS,
        ..._opts
    } as Required<ExportOptions>;

    const { context } = opts;

    const exporter = new GLTFExporter();
    exporter.register(writer => new GLTFMeshGPUInstancingExtension(writer));
    exporter.register(writer => new GLTFGizmoWriter(writer));
    exporter.register(writer => new GLTFRenderTextureWriter(writer));
    registerExportExtensions(exporter, opts.context);

    const exporterOptions: GLTFExporterOptions = {
        binary: opts.binary,
        animations: collectAnimations(context, opts.scene, []),
    }
    const state = new ExporterState();
    console.debug("Exporting GLTF", exporterOptions);
    state.onBeforeExport(opts);
    __isExporting(true);
    const res = await exporter.parseAsync(opts.scene, exporterOptions).catch((e) => {
        console.error(e);
        return null;
    });
    __isExporting(false);
    state.onAfterExport(opts);

    if (!res) {
        throw new Error("Failed to export GLTF");
    }

    if (opts.downloadAs != undefined) {
        let blob: Blob | null = null;
        if (res instanceof ArrayBuffer) {
            blob = new Blob([res], { type: "application/octet-stream" });
        }
        else {
            console.error("Can not download GLTF as a blob", res);
        }

        if (blob) {
            const url = URL.createObjectURL(blob);
            const a = document.createElement("a");
            a.href = url;
            let name = opts.downloadAs;
            if (!name.endsWith(".glb") && !name.endsWith(".gltf")) {
                name += opts.binary ? ".glb" : ".gltf";
            }
            a.download = name;
            a.click();
        }
    }

    return res;
}


const ACTIONS_WEIGHT_KEY = Symbol("needle:weight");

class ExporterState {

    private readonly _undo: Array<() => void> = [];

    onBeforeExport(opts: Required<ExportOptions>) {
        opts.context.animations.mixers.forEach(mixer => {
            const actions = AnimationUtils.tryGetActionsFromMixer(mixer);
            if (actions) {
                for (let i = 0; i < actions.length; i++) {
                    const action = actions[i];
                    action[ACTIONS_WEIGHT_KEY] = action.weight;
                    action.weight = 0;
                    this._undo.push(() => { action.weight = action[ACTIONS_WEIGHT_KEY]; });
                }
            }
            mixer.update(0);
        });

        opts.context.scene.traverse(obj => {
            if(!shouldExport_HideFlags(obj)) {
                const parent = obj.parent;
                if(parent) {
                    obj.removeFromParent();
                    this._undo.push(() => parent.add(obj));
                }
            }
        });
    }

    onAfterExport(_opts: Required<ExportOptions>) {
        this._undo.forEach(fn => fn());
        this._undo.length = 0;
    }

}


function collectAnimations(context: Context, scene: Object3D | Array<Object3D>, clips: Array<AnimationClip>): Array<AnimationClip> {

    // Get all animations that are used by any mixer in the scene
    // technically we might also collect animations here that aren't used by any object in the scene because they're part of another scene
    // But that's a problem for later...
    context.animations.mixers.forEach(mixer => {
        const actions = AnimationUtils.tryGetActionsFromMixer(mixer);
        if (actions) {
            for (let i = 0; i < actions.length; i++) {
                const action = actions[i];
                const clip = action.getClip();
                // TODO: might need to check if the clip is part of the scene that we want to export
                clips.push(clip);
            }
        }
    });

    // Get all animations that are directly assigned to objects in the scene
    if (!Array.isArray(scene)) scene = [scene];
    for (const obj of scene) {
        AnimationUtils.tryGetAnimationClipsFromObjectHierarchy(obj, clips);
    }

    // ensure we only have unique clips
    const uniqueClips = new Set(clips);
    return Array.from(uniqueClips);

}