import { SceneNode } from '../../../scene/scene_node.js';
import { Scene } from '../../../scene/scene.js';
import { defineProps } from '../types.js';
import { applyPatch, DRef, diff, ASSERT, radian2degree, degree2radian } from '@zephyr3d/base';
import '../../../values.js';
import '../../../render/render_queue.js';
import '../../../render/primitive.js';
import '@zephyr3d/device';
import '../../../material/shader/helper.js';
import '../../../material/lambert.js';
import '../../../material/blinn.js';
import '../../../material/unlit.js';
import '../../../material/particle.js';
import '../../../material/meshmaterial.js';
import '../../../material/grassmaterial.js';
import '../../../material/terrain-cm.js';
import '../../../material/pbrmr.js';
import '../../../material/pbrsg.js';
import '../../../material/pbrblueprint.js';
import '../../../material/sprite.js';
import '../../../material/sprite_std.js';
import '../../blueprint/material/texture.js';
import '../../../render/rendergraph/forward_plus_builder.js';
import '../../../render/sky.js';
import '../../../render/clipmap.js';
import '../../../shaders/atmosphere.js';
import { GraphNode } from '../../../scene/graph_node.js';
import '../../../shadow/shadowmapper.js';
import '../../../scene/mesh.js';
import '../../../scene/basesprite.js';
import '../../../scene/water.js';
import '../../../scene/particlesys.js';
import '../../../scene/batchgroup.js';
import '../../../scene/terrain-cm/terrain-cm.js';
import '../../../scene/terrain-cm/grass.js';
import '../../../scene/raycast_visitor.js';
import { ScriptAttachment, normalizeScriptAttachmentConfig } from '../../../scene/script_attachment.js';
import { AnimationClip } from '../../../animation/animation.js';
import { NodeRotationTrack } from '../../../animation/rotationtrack.js';
import '../../../animation/eulerrotationtrack.js';
import { NodeTranslationTrack } from '../../../animation/translationtrack.js';
import { NodeScaleTrack } from '../../../animation/scaletrack.js';
import '../../../animation/skeleton.js';
import { restoreGeometryCacheMeshBinding } from '../../../animation/geometry_cache_utils.js';
import '../../../animation/joint_dynamics/collision.js';
import '../../../animation/joint_dynamics/solver.js';
import '../../../animation/joint_dynamics/joint_dynamics_system.js';
import { JSONData, JSONArray } from '../json.js';
import { parseZABCBlob, attachZABCAnimationsToSceneNode } from '../../../asset/loaders/zabc/zabc_loader.js';

const geometryCacheBindings = new WeakMap();
function normalizeSerializedSceneNodeData(data) {
    if (!data || typeof data !== 'object' || Array.isArray(data)) {
        return {
            ClassName: 'SceneNode',
            Object: {}
        };
    }
    const serialized = data;
    if (typeof serialized.ClassName === 'string') {
        return serialized;
    }
    const objectData = serialized.Object && typeof serialized.Object === 'object' && !Array.isArray(serialized.Object) ? serialized.Object : serialized;
    return {
        ClassName: 'SceneNode',
        Object: objectData
    };
}
function hasMeshDescendants(node) {
    let hasMesh = false;
    node.iterate((child)=>{
        if (child.isMesh()) {
            hasMesh = true;
            return true;
        }
        return false;
    });
    return hasMesh;
}
async function restoreGeometryCacheMeshes(node) {
    const tasks = [];
    node.iterate((child)=>{
        if (child.isMesh()) {
            tasks.push(restoreGeometryCacheMeshBinding(child));
        }
        return false;
    });
    if (tasks.length > 0) {
        await Promise.all(tasks);
    }
}
async function clearGeometryCacheBinding(node) {
    const previous = geometryCacheBindings.get(node);
    if (!previous) {
        return null;
    }
    for (const name of previous.animationNames){
        if (node.animationSet.isPlayingAnimation(name)) {
            node.animationSet.stopAnimation(name);
        }
        node.animationSet.deleteAnimation(name);
    }
    await restoreGeometryCacheMeshes(node);
    geometryCacheBindings.delete(node);
    return previous;
}
/** @internal */ function getSceneNodeClass(manager) {
    return {
        ctor: SceneNode,
        name: 'SceneNode',
        async createFunc (ctx, init) {
            const scene = ctx instanceof Scene ? ctx : ctx.scene;
            if (init?.prefabId) {
                const prefabData = (await manager.loadPrefabContent(init.prefabId)).data;
                const nodeData = normalizeSerializedSceneNodeData(applyPatch(prefabData, init.patch ?? []));
                const tmpNode = new DRef(new SceneNode(scene));
                tmpNode.get().remove();
                tmpNode.get().prefabId = init.prefabId;
                const sceneNode = await manager.deserializeObject(tmpNode.get(), nodeData);
                if (sceneNode) {
                    sceneNode.prefabId = init.prefabId;
                    sceneNode.parent = ctx instanceof SceneNode ? ctx : ctx.rootNode;
                }
                tmpNode.dispose();
                return {
                    obj: sceneNode,
                    loadProps: false
                };
            }
            if (init?.assetId) {
                const fetchedModel = await manager.fetchModel(init.assetId, scene);
                const sceneNode = fetchedModel?.group ?? null;
                if (sceneNode) {
                    const originalAssetId = manager.getAssetId(sceneNode);
                    try {
                        manager.setAssetId(sceneNode, null);
                        const baseNodeData = await manager.serializeObject(sceneNode);
                        const nodeData = normalizeSerializedSceneNodeData(applyPatch(baseNodeData, init.patch ?? []));
                        await manager.deserializeObjectProps(sceneNode, nodeData.Object);
                    } finally{
                        manager.setAssetId(sceneNode, originalAssetId ?? init.assetId);
                    }
                    sceneNode.parent = ctx instanceof SceneNode ? ctx : ctx.rootNode;
                }
                return {
                    obj: sceneNode,
                    loadProps: false
                };
            }
            const node = new SceneNode(scene);
            if (ctx instanceof SceneNode) {
                node.parent = ctx;
            }
            return {
                obj: node
            };
        },
        async getInitParams (obj, flags) {
            const prefabId = obj.prefabId;
            const assetId = manager.getAssetId(obj);
            let patch = undefined;
            if (prefabId) {
                try {
                    obj.prefabId = '';
                    const prefabData = (await manager.loadPrefabContent(prefabId)).data;
                    const nodeData = await manager.serializeObject(obj);
                    patch = diff(prefabData, nodeData);
                    ASSERT(diff(applyPatch(prefabData, patch), nodeData).length === 0, 'Patch test failed');
                } finally{
                    obj.prefabId = prefabId;
                }
                flags.saveProps = false;
            }
            if (!prefabId && assetId) {
                const baseNode = (await manager.fetchModel(assetId, obj.scene))?.group ?? null;
                if (baseNode) {
                    const originalObjectAssetId = manager.getAssetId(obj);
                    const originalBaseAssetId = manager.getAssetId(baseNode);
                    try {
                        manager.setAssetId(obj, null);
                        manager.setAssetId(baseNode, null);
                        const baseNodeData = await manager.serializeObject(baseNode);
                        const nodeData = await manager.serializeObject(obj);
                        patch = diff(baseNodeData, nodeData);
                        ASSERT(diff(applyPatch(baseNodeData, patch), nodeData).length === 0, 'Patch test failed');
                    } finally{
                        manager.setAssetId(obj, originalObjectAssetId);
                        manager.setAssetId(baseNode, originalBaseAssetId);
                        baseNode.remove();
                    }
                    flags.saveProps = false;
                }
            }
            return prefabId ? {
                prefabId,
                patch
            } : assetId ? {
                assetId,
                patch
            } : null;
        },
        getProps () {
            return defineProps([
                {
                    name: 'Id',
                    description: 'Unique persistent id for the node',
                    type: 'string',
                    get (value) {
                        value.str[0] = this.persistentId;
                    },
                    set (value) {
                        this.persistentId = value.str[0];
                    }
                },
                {
                    name: 'PrefabId',
                    description: 'Persistent ID of the prefab source for this node',
                    type: 'string',
                    isHidden () {
                        return true;
                    },
                    get (value) {
                        value.str[0] = this.prefabId;
                    }
                },
                {
                    name: 'Name',
                    description: 'Display name of the node',
                    type: 'string',
                    getDefaultValue () {
                        return '';
                    },
                    get (value) {
                        value.str[0] = this.name;
                    },
                    set (value) {
                        this.name = value.str[0];
                    }
                },
                {
                    name: 'Position',
                    description: 'Local position of the node',
                    type: 'vec3',
                    default: [
                        0,
                        0,
                        0
                    ],
                    options: {
                        animatable: true
                    },
                    isPersistent () {
                        return this.jointTypeT !== 'animated';
                    },
                    get (value) {
                        value.num[0] = this.position.x;
                        value.num[1] = this.position.y;
                        value.num[2] = this.position.z;
                    },
                    set (value) {
                        this.position.setXYZ(value.num[0], value.num[1], value.num[2]);
                    }
                },
                {
                    name: 'Scale',
                    description: 'Local scale of the node',
                    type: 'vec3',
                    default: [
                        1,
                        1,
                        1
                    ],
                    options: {
                        animatable: true
                    },
                    isPersistent () {
                        return this.jointTypeS !== 'animated';
                    },
                    isHidden () {
                        return this.isCamera();
                    },
                    get (value) {
                        value.num[0] = this.scale.x;
                        value.num[1] = this.scale.y;
                        value.num[2] = this.scale.z;
                    },
                    set (value) {
                        this.scale.setXYZ(value.num[0], value.num[1], value.num[2]);
                    }
                },
                {
                    name: 'Rotation',
                    description: 'Euler rotation of this node in ZYX order',
                    type: 'vec3',
                    default: [
                        0,
                        0,
                        0
                    ],
                    options: {
                        animatable: true,
                        edit: 'quaternion'
                    },
                    isPersistent () {
                        return false;
                    },
                    isHidden () {
                        return this.isCamera() && this.isOrtho();
                    },
                    get (value) {
                        const zyx = this.rotation.toEulerAngles();
                        value.num[0] = Math.round(radian2degree(zyx.x));
                        value.num[1] = Math.round(radian2degree(zyx.y));
                        value.num[2] = Math.round(radian2degree(zyx.z));
                    },
                    set (value) {
                        this.rotation.fromEulerAngle(degree2radian(value.num[0]), degree2radian(value.num[1]), degree2radian(value.num[2]));
                    }
                },
                {
                    name: 'QuatRotation',
                    description: 'Node rotation quaternion',
                    type: 'vec4',
                    default: [
                        0,
                        0,
                        0,
                        1
                    ],
                    isHidden () {
                        return true;
                    },
                    isPersistent () {
                        return this.jointTypeR !== 'animated';
                    },
                    get (value) {
                        value.num[0] = this.rotation.x;
                        value.num[1] = this.rotation.y;
                        value.num[2] = this.rotation.z;
                        value.num[3] = this.rotation.w;
                    },
                    set (value) {
                        this.rotation.setXYZW(value.num[0], value.num[1], value.num[2], value.num[3]);
                    }
                },
                {
                    name: 'Pickable',
                    description: 'If true, enable ray-cast for this object',
                    type: 'bool',
                    default: false,
                    get (value) {
                        value.bool[0] = this.pickable;
                    },
                    set (value) {
                        this.pickable = value.bool[0];
                    }
                },
                {
                    name: 'Visible',
                    description: 'Node visible state',
                    type: 'string',
                    default: 'inherit',
                    options: {
                        enum: {
                            labels: [
                                'Visible',
                                'Hidden',
                                'Inherit'
                            ],
                            values: [
                                'visible',
                                'hidden',
                                'inherit'
                            ]
                        }
                    },
                    get (value) {
                        value.str[0] = this.showState;
                    },
                    set (value) {
                        this.showState = value.str[0];
                    }
                },
                {
                    name: 'Children',
                    description: 'Children nodes of this node',
                    type: 'object_array',
                    phase: 0,
                    isHidden () {
                        return true;
                    },
                    get (value) {
                        value.object = [];
                        for (const child of this.children){
                            if (!child.sealed) {
                                value.object.push(child);
                            }
                        }
                    },
                    set (value) {
                        const children = this.children;
                        for(let i = children.length - 1; i >= 0; i--){
                            const child = children[i];
                            if (!value.object.includes(child) && !child.sealed) {
                                child.remove();
                            }
                        }
                        for (const child of value.object){
                            if (child instanceof SceneNode) {
                                child.parent = this;
                            } else {
                                console.error(`Invalid scene node: ${child}`);
                            }
                        }
                    }
                },
                {
                    name: 'GeometryCache',
                    description: 'Alembic geometry cache asset used by this node',
                    type: 'string',
                    options: {
                        group: 'Animation',
                        label: 'Geometry Cache',
                        mimeTypes: [
                            'application/vnd.zephyr3d.alembic-cache+json'
                        ]
                    },
                    isValid () {
                        return this !== this.scene?.rootNode && hasMeshDescendants(this);
                    },
                    get (value) {
                        value.str[0] = geometryCacheBindings.get(this)?.assetId ?? '';
                    },
                    async set (value) {
                        const previous = await clearGeometryCacheBinding(this);
                        const assetId = value?.str?.[0]?.trim() ?? '';
                        if (!assetId) {
                            return;
                        }
                        const binary = await manager.assetManager.fetchBinaryData(assetId);
                        const parsed = await parseZABCBlob(new Blob([
                            binary
                        ], {
                            type: manager.VFS.guessMIMEType(assetId) || 'application/octet-stream'
                        }));
                        const autoPlay = previous?.autoPlay ?? false;
                        const result = await attachZABCAnimationsToSceneNode(this, parsed, {
                            sourcePath: assetId,
                            autoPlay,
                            replaceAnimationNames: []
                        });
                        geometryCacheBindings.set(this, {
                            assetId,
                            autoPlay,
                            animationNames: result.animationNames
                        });
                    }
                },
                {
                    name: 'GeometryCacheAutoPlay',
                    description: 'If true, plays the geometry cache automatically when loaded',
                    type: 'bool',
                    default: false,
                    options: {
                        group: 'Animation',
                        label: 'Cache AutoPlay'
                    },
                    isValid () {
                        return this !== this.scene?.rootNode && hasMeshDescendants(this);
                    },
                    get (value) {
                        value.bool[0] = geometryCacheBindings.get(this)?.autoPlay ?? false;
                    },
                    async set (value) {
                        const current = geometryCacheBindings.get(this);
                        if (!current) {
                            return;
                        }
                        const autoPlay = !!value.bool[0];
                        geometryCacheBindings.set(this, {
                            assetId: current.assetId,
                            autoPlay,
                            animationNames: current.animationNames
                        });
                        for (const name of current.animationNames){
                            const animation = this.animationSet.getAnimationClip(name);
                            if (animation) {
                                animation.autoPlay = autoPlay;
                                if (autoPlay) {
                                    this.animationSet.playAnimation(name, {
                                        repeat: 0
                                    });
                                } else if (this.animationSet.isPlayingAnimation(name)) {
                                    this.animationSet.stopAnimation(name);
                                }
                            }
                        }
                    }
                },
                {
                    name: 'Animations',
                    description: 'Animation clips which affect this object or children',
                    type: 'object_array',
                    phase: 2,
                    readonly: true,
                    options: {
                        objectTypes: [
                            AnimationClip
                        ]
                    },
                    get (value) {
                        const animationSet = this.animationSet;
                        value.object = animationSet.getAnimationNames().map((name)=>animationSet.getAnimationClip(name));
                    },
                    set (value) {
                        for (const ani of value.object){
                            const animation = ani;
                            for (const tracks of animation.tracks){
                                for (const track of tracks[1]){
                                    if (!track.embedded) {
                                        animation.addTrack(tracks[0], track);
                                        if (tracks[0] instanceof SceneNode) {
                                            if (track instanceof NodeTranslationTrack) {
                                                tracks[0].jointTypeT = 'animated';
                                            } else if (track instanceof NodeScaleTrack) {
                                                tracks[0].jointTypeS = 'animated';
                                            } else if (track instanceof NodeRotationTrack) {
                                                tracks[0].jointTypeR = 'animated';
                                            }
                                        }
                                    }
                                }
                            }
                            if (!manager.editorMode && animation.autoPlay) {
                                this.animationSet.playAnimation(animation.name, {
                                    repeat: 0
                                });
                            }
                        }
                    },
                    delete (index) {
                        const animationSet = this.animationSet;
                        const name = animationSet.getAnimationNames()[index];
                        const animation = animationSet.getAnimationClip(name);
                        if (animation) {
                            animationSet.deleteAnimation(name);
                        }
                    }
                },
                {
                    name: 'Skeletons',
                    description: 'Skeleton references used by the node animations',
                    type: 'object_array',
                    phase: 1,
                    isHidden () {
                        return true;
                    },
                    get (value) {
                        const animationSet = this.animationSet;
                        value.object = animationSet.skeletons.map((v)=>v.get());
                    },
                    set (value) {
                        const animationSet = this.animationSet;
                        animationSet.skeletons.forEach((v)=>v.dispose());
                        animationSet.skeletons.splice(0, animationSet.skeletons.length);
                        animationSet.skeletons.push(...value.object.map((v)=>new DRef(v)));
                    }
                },
                {
                    name: 'Script',
                    description: 'First script file path which is attached to this node',
                    type: 'object',
                    options: {
                        mimeTypes: [
                            'text/x-typescript'
                        ]
                    },
                    isHidden () {
                        return true;
                    },
                    isNullable () {
                        return true;
                    },
                    get (value) {
                        value.str[0] = this.script;
                    },
                    set (value) {
                        this.script = value?.str?.[0] ?? '';
                    }
                },
                {
                    name: 'Scripts',
                    description: 'All script file paths which are attached to this node',
                    type: 'object_array',
                    options: {
                        objectTypes: [
                            ScriptAttachment
                        ]
                    },
                    isHidden () {
                        return true;
                    },
                    getDefaultValue () {
                        return this.scripts;
                    },
                    isNullable () {
                        return false;
                    },
                    get (value) {
                        value.object = this.scripts;
                    },
                    set (value) {
                        this.scripts = value.object ?? [];
                    },
                    create () {
                        return new ScriptAttachment();
                    },
                    add (value, index) {
                        const attachments = this.scripts;
                        attachments.splice(index ?? attachments.length, 0, value.object?.[0] ?? new ScriptAttachment());
                        this.scripts = attachments;
                    },
                    delete (index) {
                        const attachments = this.scripts;
                        if (index >= 0 && index < attachments.length) {
                            attachments.splice(index, 1);
                            this.scripts = attachments;
                        }
                    }
                },
                {
                    name: 'ScriptConfig',
                    description: 'Configuration object passed to the attached script',
                    type: 'object',
                    options: {
                        objectTypes: [
                            JSONData,
                            JSONArray
                        ]
                    },
                    isHidden () {
                        return true;
                    },
                    isNullable () {
                        return true;
                    },
                    isPersistent () {
                        return !!this.script;
                    },
                    get (value) {
                        const config = normalizeScriptAttachmentConfig(this.scriptConfig);
                        value.object[0] = config == null ? null : Array.isArray(config) ? new JSONArray(null, config) : new JSONData(null, config);
                    },
                    set (value) {
                        const data = value?.object[0];
                        this.scriptConfig = data instanceof JSONData || data instanceof JSONArray ? normalizeScriptAttachmentConfig(data.data) : normalizeScriptAttachmentConfig(data);
                    }
                },
                {
                    name: 'ScriptConfigs',
                    description: 'Per-script configuration array for all attached scripts',
                    type: 'object',
                    options: {
                        objectTypes: [
                            JSONArray
                        ]
                    },
                    getDefaultValue () {
                        return this.scriptConfigs;
                    },
                    isHidden () {
                        return true;
                    },
                    isNullable () {
                        return true;
                    },
                    isPersistent () {
                        return this.scripts.length > 0;
                    },
                    get (value) {
                        value.object[0] = this.scriptConfigs.length > 0 ? new JSONArray(null, this.scriptConfigs) : null;
                    },
                    set (value) {
                        const data = value?.object[0];
                        this.scriptConfigs = data instanceof JSONArray ? data.data : Array.isArray(data) ? data : [];
                    }
                },
                {
                    name: 'Metadata',
                    description: 'Meta data for this node',
                    type: 'object',
                    options: {
                        objectTypes: [
                            JSONData
                        ]
                    },
                    isNullable () {
                        return true;
                    },
                    get (value) {
                        value.object[0] = this.metaData ? new JSONData(null, this.metaData) : null;
                    },
                    set (value) {
                        const data = value?.object[0];
                        this.metaData = data instanceof JSONData ? data.data : data ?? null;
                    }
                }
            ]);
        }
    };
}
/** @internal */ function getGraphNodeClass() {
    return {
        ctor: GraphNode,
        parent: SceneNode,
        name: 'GraphNode',
        createFunc (ctx) {
            const node = new GraphNode(ctx.scene);
            node.parent = ctx;
            return {
                obj: node
            };
        },
        getProps () {
            return [];
        }
    };
}

export { getGraphNodeClass, getSceneNodeClass };
//# sourceMappingURL=node.js.map
