import { DWeakRef, ASSERT, base64ToUint8Array, Vector3, DRef, Vector4, VFSError, isPowerOf2, nextPowerOf2 } from '@zephyr3d/base';
import { GLTFLoader } from './loaders/gltf/gltf_loader.js';
import { WebImageLoader } from './loaders/image/webimage_loader.js';
import { DDSLoader } from './loaders/dds/dds_loader.js';
import { HDRLoader } from './loaders/hdr/hdr.js';
import { Primitive } from '../render/primitive.js';
import '@zephyr3d/device';
import { getEngine, getDevice } from '../app/api.js';
import { CopyBlitter } from '../blitter/copy.js';
import { getSheenLutLoader } from './builtin.js';
import { BUILTIN_ASSET_TEXTURE_SHEEN_LUT } from '../values.js';
import { TGALoader } from './loaders/image/tga_Loader.js';
import '../material/shader/helper.js';
import '../material/lambert.js';
import '../material/blinn.js';
import '../material/unlit.js';
import '../material/particle.js';
import { Material } from '../material/material.js';
import '../material/meshmaterial.js';
import '../material/grassmaterial.js';
import '../material/terrain-cm.js';
import '../material/pbrmr.js';
import '../material/pbrsg.js';
import { PBRBluePrintMaterial } from '../material/pbrblueprint.js';
import '../material/sprite.js';
import '../material/sprite_std.js';
import { SpriteBlueprintMaterial } from '../material/spriteblueprint.js';
import { BoundingBox } from '../utility/bounding_volume.js';
import { MaterialBlueprintIR } from '../utility/blueprint/material/ir.js';
import '../render/render_queue.js';
import '../render/rendergraph/forward_plus_builder.js';
import '../render/sky.js';
import '../render/clipmap.js';
import '../shaders/atmosphere.js';

/**
 * Centralized asset manager for loading and caching resources.
 *
 * Responsibilities:
 * - Abstracts resource loading via VFS (text/json/binary).
 * - Dispatches texture/model loading to registered loaders by MIME type.
 * - Caches results and uses weak references to allow GPU resources to be GC'd when unused.
 * - Harmonizes cross-backend constraints (e.g., WebGL non-power-of-two rules and sRGB handling).
 * - Provides access to built-in textures with device-restore handlers.
 *
 * Threading/async model:
 * - All I/O is async; repeated calls are coalesced via internal promise caches keyed by URL or hash.
 *
 * @public
 */ class AssetManager {
    /** @internal */ static _builtinTextures = {};
    /** @internal */ static _builtinTextureLoaders = {
        [BUILTIN_ASSET_TEXTURE_SHEEN_LUT]: getSheenLutLoader(64)
    };
    /** @internal */ static _textureLoaders = [
        new WebImageLoader(),
        new DDSLoader(),
        new HDRLoader(),
        new TGALoader()
    ];
    /** @internal */ static _modelLoaders = [
        new GLTFLoader()
    ];
    /** @internal */ _textures;
    /** @internal */ _models;
    /** @internal */ _binaryDatas;
    /** @internal */ _textDatas;
    /** @internal */ _bluePrints;
    /** @internal */ _materials;
    /** @internal */ _primitives;
    /** @internal */ _skeletons;
    /** @internal */ _jsonDatas;
    /** @internal */ _resourceManager;
    /**
   * Creates an instance of AssetManager
   */ constructor(resourceManager){
        this._resourceManager = resourceManager ?? getEngine().resourceManager;
        this._textures = {};
        this._models = {};
        this._materials = {};
        this._primitives = {};
        this._skeletons = {};
        this._bluePrints = {};
        this._binaryDatas = {};
        this._textDatas = {};
        this._jsonDatas = {};
    }
    /**
   * VFS used to read resources (files, URLs, virtual mounts).
   */ get vfs() {
        return this._resourceManager.VFS;
    }
    /**
   * Clear cached references and promises.
   *
   * - Disposes any DWeakRef holders maintained by this manager.
   * - Empties internal maps for textures, models, and raw data (text/json/binary).
   * - Does not forcibly dispose GPU resources; it only clears references so they can be GC'd
   *   if no other owners are holding them.
   */ clearCache() {
        for(const k in Object.keys(this._textures)){
            const v = this._textures[k];
            if (v instanceof DWeakRef) {
                v.dispose();
            }
        }
        this._textures = {};
        for(const k in Object.keys(this._models)){
            const v = this._models[k];
            if (v instanceof DWeakRef) {
                v.dispose();
            }
        }
        this._models = {};
        for(const k in Object.keys(this._materials)){
            const v = this._materials[k];
            if (v instanceof DWeakRef) {
                v.dispose();
            }
        }
        this._materials = {};
        for(const k in Object.keys(this._primitives)){
            const v = this._primitives[k];
            if (v instanceof DWeakRef) {
                v.dispose();
            }
        }
        this._primitives = {};
        for(const k in Object.keys(this._skeletons)){
            const v = this._skeletons[k];
            if (v instanceof DWeakRef) {
                v.dispose();
            }
        }
        this._skeletons = {};
        this._binaryDatas = {};
        this._textDatas = {};
        this._jsonDatas = {};
    }
    /**
   * Register a texture loader (highest priority first).
   *
   * Note: This is a static registry shared by all AssetManager instances.
   *
   * @param loader - A concrete texture loader implementation.
   */ static addTextureLoader(loader) {
        if (loader) {
            this._textureLoaders.unshift(loader);
        }
    }
    /**
   * Register a model loader (highest priority first).
   *
   * Note: This is a static registry shared by all AssetManager instances.
   *
   * @param loader - A concrete model loader implementation.
   */ static addModelLoader(loader) {
        if (loader) {
            this._modelLoaders.unshift(loader);
        }
    }
    /**
   * Fetch a UTF-8 text resource via VFS.
   *
   * - Results are cached per resolved URL (via HttpRequest.urlResolver if provided; otherwise the raw URL).
   * - If cached, any provided postProcess is ignored for subsequent calls; create a separate AssetManager
   *   if you need different post-processing of the same URL.
   *
   * @param url - Resource URL or VFS path.
   * @param postProcess - Optional transformation applied to the loaded text.
   * @param httpRequest - Optional HttpRequest for custom URL resolution/headers.
   * @returns A promise that resolves to the loaded (and optionally processed) text.
   */ async fetchTextData(url, postProcess, httpRequest, VFSs) {
        const hash = httpRequest?.urlResolver?.(url) ?? url;
        let P = this._textDatas[hash];
        if (!P) {
            P = this.loadTextData(url, postProcess, VFSs);
            this._textDatas[hash] = P;
        }
        return P;
    }
    /**
   * Fetch a JSON resource via VFS.
   *
   * - Parses as JSON after text load.
   * - Cached per resolved URL. Post-process is applied only on the first load for a given cache key.
   *
   * @param url - Resource URL or VFS path.
   * @param postProcess - Optional transformation applied to the parsed JSON object.
   * @param httpRequest - Optional HttpRequest for custom URL resolution/headers.
   * @returns A promise that resolves to the loaded (and optionally processed) JSON value.
   */ async fetchJsonData(url, postProcess, httpRequest, VFSs) {
        const hash = httpRequest?.urlResolver?.(url) ?? url;
        let P = this._jsonDatas[hash];
        if (!P) {
            P = this.loadJsonData(url, postProcess, VFSs);
            this._jsonDatas[hash] = P;
        }
        return P;
    }
    /**
   * Fetch a binary resource via VFS.
   *
   * - Cached per resolved URL. Post-process is applied only on first load for a given key.
   *
   * @param url - Resource URL or VFS path.
   * @param postProcess - Optional transformation applied to the loaded ArrayBuffer.
   * @param httpRequest - Optional HttpRequest for custom URL resolution/headers.
   * @returns A promise that resolves to the loaded (and optionally processed) ArrayBuffer.
   */ async fetchBinaryData(url, postProcess, httpRequest, VFSs) {
        const hash = httpRequest?.urlResolver?.(url) ?? url;
        let P = this._binaryDatas[hash];
        if (!P) {
            P = this.loadBinaryData(url, postProcess, VFSs);
            this._binaryDatas[hash] = P;
        }
        return P;
    }
    async fetchBluePrint(url, VFSs) {
        const hash = url;
        let P = this._bluePrints[hash];
        if (!P) {
            P = this.loadBluePrint(url, VFSs);
            this._bluePrints[hash] = P;
        }
        return P;
    }
    /**
   * Fetch a material resource.
   *
   * @typeParam T - Expected concrete material type.
   * @param url - Resource URL or VFS path.
   * @returns A promise that resolves to the loaded material.
   */ async fetchMaterial(url, VFSs) {
        const hash = url;
        let P = this._materials[hash];
        if (P instanceof DWeakRef && P.get() && !P.get().disposed) {
            return P.get();
        } else if (!P || P instanceof DWeakRef) {
            P = this.loadMaterial(url, false, VFSs);
            this._materials[hash] = P;
        }
        const material = await P;
        if (this._materials[hash] instanceof Promise) {
            this._materials[hash] = new DWeakRef(material);
        }
        return material;
    }
    /**
   * Fetch a primitive resource.
   *
   * @typeParam T - Expected concrete primitive type.
   * @param url - Resource URL or VFS path.
   * @returns A promise that resolves to the loaded primitive.
   */ async fetchPrimitive(url, VFSs) {
        const hash = url;
        let P = this._primitives[hash];
        if (P instanceof DWeakRef && P.get() && !P.get().disposed) {
            return P.get();
        } else if (!P || P instanceof DWeakRef) {
            P = this.loadPrimitive(url, VFSs);
            this._primitives[hash] = P;
        }
        const primitive = await P;
        if (this._primitives[hash] instanceof Promise) {
            this._primitives[hash] = new DWeakRef(primitive);
        }
        return primitive;
    }
    /**
   * Fetch a texture resource via registered loaders.
   *
   * - Chooses loader by explicit MIME type or by VFS file extension inference.
   * - Deduplicates in-flight requests and caches ready textures.
   * - If `options.texture` is provided, the asset will be uploaded/blitted into that texture.
   * - On WebGL backends, enforces constraints by repacking non-power-of-two or sRGB textures.
   *
   * @typeParam T - Expected concrete texture type.
   * @param url - Resource URL or VFS path.
   * @param options - Texture fetching options (color space, sampler, target texture).
   * @param httpRequest - Optional HttpRequest (not used for binary read but may supply URL resolver for hashing).
   * @returns A promise that resolves to the loaded texture.
   */ async fetchTexture(url, options, VFSs) {
        if (options?.texture) {
            return this.loadTexture(url, options.mimeType ?? null, !options.linearColorSpace, options.samplerOptions, options.texture, VFSs);
        } else {
            const hash = this.getHash('2d', url, options);
            let P = this._textures[hash];
            if (P instanceof DWeakRef && P.get() && !P.get().disposed) {
                return P.get();
            } else if (!P || P instanceof DWeakRef) {
                P = this.loadTexture(url, options?.mimeType ?? null, !options?.linearColorSpace, options?.samplerOptions, null, VFSs);
                this._textures[hash] = P;
            }
            const tex = await P;
            if (this._textures[hash] instanceof Promise) {
                this._textures[hash] = new DWeakRef(tex);
            }
            return tex;
        }
    }
    /**
   * Fetch a model resource via registered model loaders (data only).
   *
   * - Returns a SharedModel which can create scene nodes in any Scene.
   * - Uses DWeakRef to cache and allow model data to be reclaimed if unused.
   *
   * @param url - Model URL or VFS path.
   * @param options - Model loader options (MIME override, Draco, instancing hint, post-process).
   * @returns A promise that resolves to the SharedModel.
   * @internal
   */ async fetchModelData(url, options, VFSs) {
        const hash = url;
        let P = this._models[hash];
        if (P instanceof DWeakRef && P.get() && !P.get().disposed) {
            return P.get();
        } else if (!P || P instanceof DWeakRef) {
            P = this.loadModel(url, options, VFSs);
            this._models[hash] = P;
        }
        const sharedModel = await P;
        if (this._models[hash] instanceof Promise) {
            this._models[hash] = new DWeakRef(sharedModel);
        }
        return sharedModel;
    }
    /**
   * Fetch a model resource and instantiate it under a scene.
   *
   * - Loads or retrieves a cached SharedModel, then creates a SceneNode hierarchy.
   * - Returns both the created group node and any associated AnimationSet.
   *
   * @param scene - Scene into which the model node will be created.
   * @param url - Model URL or VFS path.
   * @param options - Model loader options and instancing hint.
   * @param httpRequest - Optional HttpRequest (unused for binary read; present for API symmetry).
   * @returns A promise with the created node group and animation set info.
   */ async fetchModel(scene, url, options, VFSs) {
        const sharedModel = await this.fetchModelData(url, options, VFSs);
        const node = sharedModel.createSceneNode(scene, !!options?.enableInstancing);
        return {
            group: node,
            animationSet: node.animationSet
        };
    }
    /**
   * Load a text resource via VFS and optionally post-process it.
   *
   * - Does not use or modify the internal cache; use fetchTextData for cached loads.
   *
   * @param url - Resource URL or VFS path.
   * @param postProcess - Optional transformation applied to the text.
   * @returns A promise that resolves to the loaded (and optionally processed) text.
   * @internal
   */ async loadTextData(url, postProcess, VFSs) {
        let text = await this.readFileFromVFSs(url, {
            encoding: 'utf8'
        }, VFSs);
        if (postProcess) {
            try {
                text = postProcess(text);
            } catch (err) {
                throw new Error(`Load text data post process failed: ${err}`);
            }
        }
        return text;
    }
    /**
   * Load a JSON resource via VFS and optionally post-process it.
   *
   * - Does not use or modify the internal cache; use fetchJsonData for cached loads.
   *
   * @param url - Resource URL or VFS path.
   * @param postProcess - Optional transformation applied to the parsed JSON.
   * @returns A promise that resolves to the loaded (and optionally processed) JSON.
   * @internal
   */ async loadJsonData(url, postProcess, VFSs) {
        let json = JSON.parse(await this.readFileFromVFSs(url, {
            encoding: 'utf8'
        }, VFSs));
        if (postProcess) {
            try {
                json = postProcess(json);
            } catch (err) {
                throw new Error(`Load json data post process failed: ${err}`);
            }
        }
        return json;
    }
    /**
   * Load a binary resource via VFS and optionally post-process it.
   *
   * - Does not use or modify the internal cache; use fetchBinaryData for cached loads.
   *
   * @param url - Resource URL or VFS path.
   * @param postProcess - Optional transformation applied to the ArrayBuffer.
   * @returns A promise that resolves to the loaded (and optionally processed) ArrayBuffer.
   * @internal
   */ async loadBinaryData(url, postProcess, VFSs) {
        try {
            let data = await this.readFileFromVFSs(url, {
                encoding: 'binary'
            }, VFSs);
            if (postProcess) {
                data = postProcess(data);
            }
            return data;
        } catch (err) {
            console.error(`Load binary data failed: ${err}`);
            return null;
        }
    }
    async loadPrimitive(url, VFSs) {
        try {
            const data = await this.readFileFromVFSs(url, {
                encoding: 'utf8'
            }, VFSs);
            const content = JSON.parse(data);
            ASSERT(content.type === 'Primitive' || content.type === 'Default', `Unsupported primitive type: ${content.type}`);
            if (content.type === 'Primitive') {
                const data = content.data;
                const primitive = new Primitive();
                for(const k in data.vertices){
                    const v = data.vertices[k];
                    const vertexData = base64ToUint8Array(v.data);
                    primitive.createAndSetVertexBuffer(v.format, vertexData);
                }
                if (data.indices) {
                    const indexData = base64ToUint8Array(data.indices);
                    const indices = data.indexType === 'u16' ? new Uint16Array(indexData.buffer) : data.indexType === 'u32' ? new Uint32Array(indexData.buffer) : null;
                    if (!indices) {
                        console.error(`Invalid index type in primitive data: ${data.indexType}`);
                        return null;
                    }
                    primitive.createAndSetIndexBuffer(indices);
                }
                primitive.primitiveType = data.type;
                primitive.indexCount = data.indexCount;
                primitive.setBoundingVolume(new BoundingBox(new Vector3(data.boxMin[0], data.boxMin[1], data.boxMin[2]), new Vector3(data.boxMax[0], data.boxMax[1], data.boxMax[2])));
                return primitive;
            } else {
                const obj = await this._resourceManager.deserializeObject(null, content.data);
                if (!(obj instanceof Primitive)) {
                    if (typeof obj.dispose === 'function') {
                        obj.dispose();
                    }
                    return null;
                }
                return obj;
            }
        } catch (err) {
            console.error(`Load primitive failed: ${err}`);
            return null;
        }
    }
    async reloadBluePrintMaterials(filter) {
        const promises = [];
        const paths = [];
        for (const k of Object.keys(this._materials)){
            const m = this._materials[k];
            if (m instanceof Promise) {
                promises.push(m);
                paths.push(k);
            } else if (m instanceof DWeakRef && !!m.get()) {
                promises.push(Promise.resolve(m.get()));
                paths.push(k);
            }
        }
        const materials = await Promise.all(promises);
        for(let i = 0; i < materials.length; i++){
            const m = materials[i];
            if (m instanceof PBRBluePrintMaterial && (!filter || filter(m))) {
                const data = await this.loadBluePrintMaterialData(paths[i], true);
                if (data) {
                    m.fragmentIR = data.irFragment;
                    m.vertexIR = data.irVertex;
                    m.uniformValues = data.uniformValues;
                    m.uniformTextures = data.uniformTextures;
                }
            }
        }
    }
    async loadBluePrintMaterialData(url, reload, VFSs) {
        try {
            let irData;
            if (typeof url === 'string') {
                const data = await this.readFileFromVFSs(url, {
                    encoding: 'utf8'
                }, VFSs);
                const content = JSON.parse(data);
                ASSERT(content.type === 'PBRBluePrintMaterial' || content.type === 'SpriteBluePrintMaterial', `Unsupported material type: ${content.type}`);
                irData = content.data;
            } else {
                irData = url;
            }
            const ir = reload ? await this.loadBluePrint(irData.IR, VFSs) : await this.fetchBluePrint(irData.IR, VFSs);
            const uniformValues = irData.uniformValues.map((v)=>({
                    ...v,
                    finalValue: v.value.length === 1 ? v.value[0] : new Float32Array(v.value)
                }));
            const uniformTextures = [];
            const textures = irData.uniformTextures;
            for (const v of textures){
                const tex = await this.fetchTexture(v.texture, {
                    linearColorSpace: !v.sRGB
                }, VFSs);
                uniformTextures.push({
                    ...v,
                    finalTexture: new DRef(tex),
                    finalSampler: getDevice().createSampler({
                        addressU: v.wrapS,
                        addressV: v.wrapT,
                        minFilter: v.minFilter,
                        magFilter: v.magFilter,
                        mipFilter: v.mipFilter
                    }),
                    params: tex ? new Vector4(tex.width, tex.height, tex.depth, tex.mipLevelCount) : Vector4.zero()
                });
            }
            return {
                irFragment: ir?.['fragment'] ?? null,
                irVertex: ir?.['vertex'] ?? null,
                uniformValues,
                uniformTextures
            };
        } catch (err) {
            console.error(`Load material failed: ${err}`);
            return null;
        }
    }
    /**
   * Load a material.
   *
   * - Does not use or modify the internal cache; use fetchMaterial for cached loads.
   *
   * @param url - Resource URL or VFS path.
   * @returns A promise that resolves to the loaded material.
   * @internal
   */ async loadMaterial(url, reload, VFSs) {
        try {
            const data = await this.readFileFromVFSs(url, {
                encoding: 'utf8'
            }, VFSs);
            const content = JSON.parse(data);
            ASSERT(content.type === 'PBRBluePrintMaterial' || content.type === 'SpriteBluePrintMaterial' || content.type === 'Default', `Unsupported material type: ${content.type}`);
            let mat;
            if (content.type === 'PBRBluePrintMaterial') {
                const data = await this.loadBluePrintMaterialData(content.data, reload, VFSs);
                mat = new PBRBluePrintMaterial(data.irFragment, data.irVertex, data.uniformValues, data.uniformTextures);
            } else if (content.type === 'SpriteBluePrintMaterial') {
                const data = await this.loadBluePrintMaterialData(content.data, reload, VFSs);
                mat = new SpriteBlueprintMaterial(data.irFragment, data.uniformValues, data.uniformTextures);
            } else {
                const obj = await this._resourceManager.deserializeObject(null, content.data);
                if (!(obj instanceof Material)) {
                    if (typeof obj.dispose === 'function') {
                        obj.dispose();
                    }
                    return null;
                }
                return obj;
            }
            if (mat && content.props) {
                await this._resourceManager.deserializeObjectProps(mat, content.props);
            }
            return mat;
        } catch (err) {
            console.error(`Load material failed: ${err}`);
            return null;
        }
    }
    rebuildGraphStructure(nodes, links) {
        const gs = {
            outgoing: {},
            incoming: {}
        };
        // Initialize adjacency lists
        for(const nodeId in nodes){
            gs.outgoing[nodeId] = [];
            gs.incoming[nodeId] = [];
        }
        // Fill with links
        for (const link of links){
            const outConnection = {
                targetNodeId: link.endNodeId,
                startSlotId: link.startSlotId,
                endSlotId: link.endSlotId
            };
            const inConnection = {
                targetNodeId: link.startNodeId,
                startSlotId: link.startSlotId,
                endSlotId: link.endSlotId
            };
            gs.outgoing[link.startNodeId]?.push(outConnection);
            gs.incoming[link.endNodeId]?.push(inConnection);
        }
        return gs;
    }
    collectReachableBackward(gs, nodes, roots) {
        const reachable = new Set();
        const q = [];
        for (const r of roots){
            if (nodes[r]) {
                reachable.add(r);
                q.push(r);
            }
        }
        while(q.length > 0){
            const u = q.shift();
            const ins = gs.incoming[u] || [];
            for (const conn of ins){
                const v = conn.targetNodeId; // predecessor
                if (!reachable.has(v)) {
                    reachable.add(v);
                    q.push(v);
                }
            }
        }
        return reachable;
    }
    getReverseTopologicalOrderFromRoots(gs, nodes, roots) {
        if (!roots || roots.length === 0) {
            return {
                order: [],
                levels: []
            };
        }
        const sub = this.collectReachableBackward(gs, nodes, roots);
        if (sub.size === 0) {
            return {
                order: [],
                levels: []
            };
        }
        const outDegree = new Map();
        for (const id of sub){
            const outs = (gs.outgoing[id] || []).filter((c)=>sub.has(c.targetNodeId));
            outDegree.set(id, outs.length);
        }
        let currentLevel = Array.from(outDegree.entries()).filter(([, deg])=>deg === 0).map(([id])=>id);
        const result = [];
        const levels = [];
        while(currentLevel.length > 0){
            levels.push([
                ...currentLevel
            ]);
            result.push(...currentLevel);
            const nextLevel = [];
            for (const u of currentLevel){
                const ins = gs.incoming[u] || [];
                for (const conn of ins){
                    const v = conn.targetNodeId; // predecessor
                    if (!sub.has(v)) {
                        continue;
                    }
                    const deg = outDegree.get(v) - 1;
                    outDegree.set(v, deg);
                    if (deg === 0) {
                        nextLevel.push(v);
                    }
                }
            }
            currentLevel = nextLevel;
        }
        if (result.length !== sub.size) {
            console.warn('Subgraph contains cycles (from given roots).');
            return null;
        }
        return {
            order: result,
            levels
        };
    }
    createBluePrintDAG(nodeMap, roots, links) {
        const gs = this.rebuildGraphStructure(nodeMap, links);
        for(const k in gs.incoming){
            const node = nodeMap[k];
            for (const conn of gs.incoming[k]){
                const input = node.inputs.find((input)=>input.id === conn.endSlotId);
                input.inputNode = nodeMap[conn.targetNodeId];
                input.inputId = conn.startSlotId;
            }
        }
        return {
            graph: gs,
            nodeMap,
            roots,
            order: this.getReverseTopologicalOrderFromRoots(gs, nodeMap, roots).order.reverse()
        };
    }
    invalidateBluePrint(path) {
        delete this._bluePrints[path];
    }
    async loadBluePrint(path, VFSs) {
        try {
            const content = await this.readFileFromVFSs(path, {
                encoding: 'utf8'
            }, VFSs);
            const bp = JSON.parse(content);
            ASSERT(bp.type === 'PBRMaterial' || bp.type === 'SpriteMaterial' || bp.type === 'MaterialFunction', `Unsupported blueprint type: ${bp.type}`);
            const states = bp.state;
            const result = {};
            for (const k of Object.keys(states)){
                const roots = [];
                const nodeMap = {};
                const state = states[k];
                for (const node of state.nodes){
                    const impl = await this._resourceManager.deserializeObject(null, node.node);
                    nodeMap[node.id] = impl;
                    if (impl.outputs.length === 0) {
                        roots.push(node.id);
                    }
                }
                const dag = await this.createBluePrintDAG(nodeMap, roots, state.links);
                result[k] = new MaterialBlueprintIR(dag, path, state);
            }
            return result;
        } catch (err) {
            const msg = `Load material failed: ${err}`;
            console.error(msg);
            return null;
        }
    }
    /**
   * Load a texture directly from an ArrayBuffer or typed array.
   *
   * - Chooses an appropriate loader based on the provided MIME type.
   * - Can upload into an existing texture if `texture` is specified.
   *
   * @typeParam T - Expected concrete texture type.
   * @param arrayBuffer - Raw texture data buffer.
   * @param mimeType - MIME type of the texture (must be supported by a registered loader).
   * @param srgb - If true, treat image as sRGB; otherwise linear.
   * @param samplerOptions - Optional sampler options passed to the loader path.
   * @param texture - Optional destination texture to populate.
   * @returns A promise that resolves to the created or populated texture.
   */ async loadTextureFromBuffer(arrayBuffer, mimeType, srgb, samplerOptions, texture) {
        for (const loader of AssetManager._textureLoaders){
            if (!loader.supportMIMEType(mimeType)) {
                continue;
            }
            const tex = await this.doLoadTexture(loader, mimeType, arrayBuffer, !!srgb, samplerOptions, texture);
            return tex;
        }
        throw new Error(`Can not find loader for MIME type '${mimeType}'`);
    }
    /**
   * Load a texture via VFS by URL and MIME type.
   *
   * - Uses the first loader that supports the inferred or provided MIME type.
   * - On WebGL, may repack textures (resample to power-of-two, convert formats) to meet backend constraints.
   * - If `texture` is provided, the source is blitted into it, possibly resizing or changing sampling accordingly.
   *
   * @param url - Texture URL or VFS path.
   * @param mimeType - Optional explicit MIME type; otherwise inferred by VFS.
   * @param srgb - If true, treat image as sRGB; otherwise linear.
   * @param samplerOptions - Optional sampler options for loader or blit path.
   * @param texture - Optional destination texture to populate.
   * @returns A promise that resolves to the created or populated texture.
   * @internal
   */ async loadTexture(url, mimeType, srgb, samplerOptions, texture, VFSs) {
        const data = await this.readFileFromVFSs(url, {
            encoding: 'binary'
        }, VFSs);
        mimeType = mimeType ?? this.vfs.guessMIMEType(url);
        for (const loader of AssetManager._textureLoaders){
            if (!loader.supportMIMEType(mimeType)) {
                continue;
            }
            const tex = await this.doLoadTexture(loader, mimeType, data, !!srgb, samplerOptions, texture);
            if (tex) {
                tex.name = this.vfs.basename(url);
            }
            return tex;
        }
        throw new Error(`Can not find loader for asset ${url}`);
    }
    /**
   * Internal routine that executes the texture load using a specific loader and applies
   * backend-specific compatibility steps (e.g., WebGL NPOT/sRGB rules).
   *
   * @param loader - Concrete loader to use for decoding/creation.
   * @param mimeType - Texture MIME type.
   * @param data - Raw binary data.
   * @param srgb - If true, treat image as sRGB; otherwise linear.
   * @param samplerOptions - Optional sampler options.
   * @param texture - Optional destination texture to populate.
   * @returns A promise that resolves to the created or populated texture.
   * @internal
   */ async doLoadTexture(loader, mimeType, data, srgb, samplerOptions, texture) {
        const device = getDevice();
        if (device.type !== 'webgl') {
            return await loader.load(mimeType, data, srgb, samplerOptions, texture);
        } else {
            let tex = await loader.load(mimeType, data, srgb, samplerOptions);
            if (tex) {
                if (texture) {
                    const magFilter = tex.width !== texture.width || tex.height !== texture.height ? 'linear' : 'nearest';
                    const minFilter = magFilter;
                    const mipFilter = 'none';
                    const sampler = device.createSampler({
                        addressU: 'clamp',
                        addressV: 'clamp',
                        magFilter,
                        minFilter,
                        mipFilter
                    });
                    const blitter = new CopyBlitter();
                    blitter.blit(tex, texture, sampler);
                    tex = texture;
                } else {
                    const po2_w = isPowerOf2(tex.width);
                    const po2_h = isPowerOf2(tex.height);
                    const srgb = tex.isSRGBFormat();
                    if (srgb || !po2_w || !po2_h) {
                        const newWidth = po2_w ? tex.width : nextPowerOf2(tex.width);
                        const newHeight = po2_h ? tex.height : nextPowerOf2(tex.height);
                        const magFilter = newWidth !== tex.width || newHeight !== tex.height ? 'linear' : 'nearest';
                        const minFilter = magFilter;
                        const mipFilter = 'none';
                        const sampler = device.createSampler({
                            addressU: 'clamp',
                            addressV: 'clamp',
                            magFilter,
                            minFilter,
                            mipFilter
                        });
                        const destFormat = srgb ? 'rgba8unorm' : tex.format;
                        const blitter = new CopyBlitter();
                        const newTexture = tex.isTexture2D() ? device.createTexture2D(destFormat, newWidth, newHeight) : device.createCubeTexture(destFormat, newWidth);
                        blitter.blit(tex, newTexture, sampler);
                        tex.dispose();
                        tex = newTexture;
                    }
                }
            }
            return tex;
        }
    }
    /**
   * Load a model via registered model loaders.
   *
   * - Selects loader by MIME type (explicit or inferred).
   * - Optionally applies a post-process transform to the SharedModel.
   * - Sets the model's name from the source filename for convenience.
   *
   * @param url - Model URL or VFS path.
   * @param options - Model load options (MIME override, Draco module, post-process hook).
   * @returns A promise that resolves to the loaded SharedModel.
   * @internal
   */ async loadModel(url, options, VFSs) {
        const arrayBuffer = await this.readFileFromVFSs(url, {
            encoding: 'binary'
        }, VFSs);
        const mimeType = options?.mimeType || this.vfs.guessMIMEType(url);
        const data = new Blob([
            arrayBuffer
        ], {
            type: mimeType
        });
        const filename = this.vfs.basename(url);
        for (const loader of AssetManager._modelLoaders){
            if (!loader.supportMIMEType(mimeType)) {
                continue;
            }
            let model = await loader.load(this, url, options?.mimeType || data.type, data, options?.dracoDecoderModule, VFSs);
            if (!model) {
                throw new Error(`Load asset failed: ${url}`);
            }
            if (options?.postProcess) {
                try {
                    model = options.postProcess(model);
                } catch (err) {
                    throw new Error(`Model loader post process failed: ${err}`);
                }
            }
            model.name = filename;
            return model;
        }
        throw new Error(`Can not find loader for asset ${url}`);
    }
    /**
   * Fetch a built-in texture synchronously by name.
   *
   * - If this built-in was not created yet, the registered loader is invoked.
   * - Registers a device restore handler so the texture can be re-initialized after device loss.
   * - If an existing texture is provided, the loader uploads into it.
   *
   * @typeParam T - Expected concrete texture type.
   * @param name - Built-in texture identifier.
   * @param texture - Optional destination texture to populate.
   * @returns The built-in texture (created or populated).
   */ fetchBuiltinTexture(name, texture) {
        const loader = AssetManager._builtinTextureLoaders[name];
        if (!loader) {
            throw new Error(`Unknown builtin texture name: ${name}`);
        }
        if (texture) {
            return loader(this, texture);
        } else {
            texture = AssetManager._builtinTextures[name];
            if (!texture) {
                texture = loader(this);
                AssetManager._builtinTextures[name] = texture;
            }
            texture.restoreHandler = (tex)=>{
                loader(this, tex);
            };
            return texture;
        }
    }
    /**
   * Override or unregister the loader for a named built-in texture.
   *
   * - Passing a valid loader function sets/overrides the creation path.
   * - Passing `undefined` removes the loader mapping for the given name.
   *
   * @param name - Built-in texture identifier.
   * @param loader - Factory that creates the built-in texture using the provided AssetManager.
   */ static setBuiltinTextureLoader(name, loader) {
        if (loader) {
            this._builtinTextureLoaders[name] = loader;
        } else {
            delete this._builtinTextureLoaders[name];
        }
    }
    /**
   * Compute a cache key for texture requests.
   *
   * Includes texture type tag, URL, and color space choice to avoid cross-color-space cache collisions.
   *
   * @typeParam T - Texture type parameter (not used for runtime behavior; helps preserve generic intent).
   * @param type - Logical texture type tag (e.g., '2d', 'cube').
   * @param url - Resource URL or VFS path.
   * @param options - Texture fetch options to incorporate into the key.
   * @returns A string cache key combining type, URL, and color space choice.
   * @internal
   */ getHash(type, url, options) {
        return `${type}:${url}:${!options?.linearColorSpace}`;
    }
    /**
   * Try reading from file from a list of VFSs
   *
   * @param path - File path
   * @param options - Read options
   * @param vfsList - VFS list
   * @returns File content
   *
   * @internal
   */ async readFileFromVFSs(path, options, vfsList) {
        vfsList = vfsList ?? [
            this.vfs
        ];
        for (const vfs of vfsList){
            try {
                return await vfs.readFile(path, options);
            } catch  {}
        }
        throw new VFSError(`File does not exist: ${path}`, 'ENOENT', path);
    }
    /**
   * Write file to a list of VFSs
   *
   * @param path - File path
   * @param data - Data to write
   * @param options - Write options
   * @param vfsList - Which VFSs will be written to
   */ async writeFileToVFSs(path, data, options, vfsList) {
        vfsList = vfsList ?? [
            this.vfs
        ];
        for (const vfs of vfsList){
            await vfs.writeFile(path, data, options);
        }
    }
}

export { AssetManager };
//# sourceMappingURL=assetmanager.js.map
