import {ImportAssetOptions, ImportResult, ProcessRawOptions, RootSceneImportResult} from './IAssetImporter'
import {
    BaseEvent,
    Camera,
    EventDispatcher,
    Light,
    LinearFilter,
    LinearMipmapLinearFilter,
    LoadingManager,
    Object3D,
    TextureLoader,
} from 'three'
import {ISerializedConfig, IViewerPlugin, type ThreeViewer} from '../viewer'
import {AssetImporter, IAssetImporterEventMap} from './AssetImporter'
import {getTextureDataType} from '../three'
import {IAsset} from './IAsset'
import {
    AddObjectOptions,
    AmbientLight2,
    DirectionalLight2,
    HemisphereLight2,
    ICamera,
    iCameraCommons,
    ILight,
    iLightCommons,
    IMaterial,
    iMaterialCommons,
    IObject3D,
    iObjectCommons,
    ISceneEventMap,
    ITexture,
    OrthographicCamera2,
    PerspectiveCamera2,
    PointLight2,
    RectAreaLight2,
    SpotLight2,
    upgradeTexture,
} from '../core'
import {Importer} from './Importer'
import {MaterialManager} from './MaterialManager'
import {
    DRACOLoader2,
    FBXLoader2,
    GLTFLoader2,
    JSONMaterialLoader,
    MTLLoader2,
    OBJLoader2,
    SimpleJSONLoader,
    SVGTextureLoader,
    VideoTextureLoader,
    ZipLoader,
} from './import'
import {RGBELoader} from 'three/examples/jsm/loaders/RGBELoader.js'
import {EXRLoader} from 'three/examples/jsm/loaders/EXRLoader.js'
import {Class, getOrCall, ValOrArr} from 'ts-browser-helpers'
import {ILoader} from './IImporter'
import {AssetExporter} from './AssetExporter'
import {IExporter} from './IExporter'
import {GLTFExporter2, GLTFWriter2} from './export'
import {legacySeparateMapSamplerUVFix} from '../utils/legacy'
import type {GLTFLoaderPlugin, GLTFParser} from 'three/examples/jsm/loaders/GLTFLoader.js'
import {GLTFExporterPlugin} from 'three/examples/jsm/exporters/GLTFExporter.js'
import {ThreeSerialization} from '../utils'
import {PolyhavenMaterialGLTFLoader} from './import/PolyhavenMaterialGLTFLoader'

// todo rename to AssetImporterCacheOptions
export interface AssetManagerOptions{
    /**
     * simple memory based cache for downloaded files, default = false
     */
    simpleCache?: boolean
    /**
     * Cache Storage for downloaded files, can use with `caches.open`
     * When true and by default uses `caches.open('threepipe-assetmanager')`, set to false to disable
     * @default true
     */
    storage?: Cache | Storage | boolean
}

export interface AddAssetOptions extends AddObjectOptions{
    /**
     * Automatically set any loaded HDR, EXR file as the scene environment map
     * @default true
     */
    autoSetEnvironment?: boolean
    /**
     * Automatically set any loaded image(ITexture) file as the scene background
     */
    autoSetBackground?: boolean
}
export interface ImportAddOptions extends ImportAssetOptions, AddAssetOptions{}
export interface AddRawOptions extends ProcessRawOptions, AddAssetOptions{}

export interface AssetManagerEventMap{
    loadAsset: {data: ImportResult}
    processStateUpdate: object
}

/**
 * Asset Manager
 *
 * Utility class to manage import, export, and material management.
 * @category Asset Manager
 */
export class AssetManager extends EventDispatcher<AssetManagerEventMap> {
    readonly viewer: ThreeViewer
    readonly importer: AssetImporter
    readonly exporter: AssetExporter
    readonly materials: MaterialManager
    get storage() {
        return this.importer.storage
    }

    constructor(viewer: ThreeViewer, cacheOptions?: AssetManagerOptions) {
        super()
        this._sceneUpdated = this._sceneUpdated.bind(this)
        this.addAsset = this.addAsset.bind(this)
        this.addRaw = this.addRaw.bind(this)
        this._loaderCreate = this._loaderCreate.bind(this)
        this.addImported = this.addImported.bind(this)

        this.importer = new AssetImporter(!!viewer.getPlugin('debug'), cacheOptions)
        this.exporter = new AssetExporter()
        this.materials = new MaterialManager()
        this.viewer = viewer
        this.viewer.scene.addEventListener('addSceneObject', this._sceneUpdated)
        this.viewer.scene.addEventListener('materialChanged', this._sceneUpdated)
        this.viewer.scene.addEventListener('beforeDeserialize', this._sceneUpdated)

        this._setupGltfExtensions()
        this._setupObjectProcess()
        this._setupObjectExport()
        this._setupProcessState()
        this._addImporters()
        this._addExporters()

    }

    async addAsset<T extends ImportResult = ImportResult>(assetOrPath?: string | IAsset | IAsset[] | File | File[], options?: ImportAddOptions): Promise<(T | undefined)[]> {
        if (!this.importer || !this.viewer) return []
        const imported = await this.importer.import<T>(assetOrPath, options)
        if (!imported) {
            const path = typeof assetOrPath === 'string' ? assetOrPath : (assetOrPath as IAsset)?.path
            if (path && !path.split('?')[0].endsWith('.vjson'))
                console.warn('Threepipe AssetManager - Unable to import', assetOrPath, imported)
            return []
        }
        return this.loadImported<(T | undefined)[]>(imported, options)
    }

    // materials: IMaterial[] = []
    // textures: ITexture[] = []

    // todo move this function to viewer
    async loadImported<T extends ValOrArr<ImportResult | undefined> = ImportResult>(imported: T, {
        autoSetEnvironment = true,
        autoSetBackground = false,
        ...options
    }: AddAssetOptions = {}): Promise<T> {
        const arr: (ImportResult | undefined)[] = Array.isArray(imported) ? imported : [imported]
        let ret: T = Array.isArray(imported) ? [] : undefined as any

        if (options?.importConfig !== false) {
            const config = arr.find(v => v?.assetType === 'config') || arr.find(v=>v && !!v.importedViewerConfig)?.importedViewerConfig
            if (config) legacySeparateMapSamplerUVFix(config, arr.filter(a=>a?.isObject3D) as Object3D[])
        }

        for (const obj of arr) {
            if (!obj) {
                if (Array.isArray(ret)) ret.push(undefined)
                continue
            }

            let r = obj

            const rootPath = obj?.__rootBlob ? obj.__rootBlob.filePath || obj.__rootBlob.name : obj.__rootPath || obj.userData?.rootPath || obj.name || ''

            switch (obj.assetType) {
            case 'material':
                this.materials.registerMaterial(<IMaterial>obj)
                break
            case 'texture':
                if (autoSetEnvironment && (rootPath?.endsWith('.hdr') || rootPath?.endsWith('.exr')))
                    this.viewer.scene.environment = <ITexture>obj
                if (autoSetBackground) this.viewer.scene.background = <ITexture>obj
                break
            case 'model':
            case 'light':
            case 'camera':
                r = await this.viewer.addSceneObject(<IObject3D | RootSceneImportResult>obj, options) // todo update references in scene update event
                break
            case 'config':
                if (options?.importConfig !== false) await this.viewer.importConfig(<ISerializedConfig>obj)
                break
            default:

                // legacy
                if (obj.type && typeof obj.type === 'string' && (Array.isArray((obj as any).plugins) ||
                        (obj as any).type === 'ThreeViewer' || this.viewer.getPlugin((obj as any).type))) {
                    await this.viewer.importConfig(<ISerializedConfig>obj)
                }
                break
            }
            this.dispatchEvent({type: 'loadAsset', data: obj})
            if (Array.isArray(ret)) ret.push(r)
            else ret = r as T
        }

        return ret
    }

    /**
     * same as {@link loadImported}
     * @param imported
     * @param options
     */
    async addProcessedAssets<T extends ImportResult | undefined = ImportResult>(imported: (T | undefined)[], options?: AddAssetOptions): Promise<(T | undefined)[]> {
        return this.loadImported(imported, options)
    }

    async addAssetSingle<T extends ImportResult = ImportResult>(asset?: string | IAsset | File, options?: ImportAssetOptions): Promise<T | undefined> {
        return !asset ? undefined : (await this.addAsset<T>(asset, options))?.[0]
    }

    // processAndAddObjects
    async addRaw<T extends (ImportResult | undefined) = ImportResult>(res: T | T[], options: AddRawOptions = {}): Promise<(T | undefined)[]> {
        const r = await this.importer.processRaw<T>(res, options)
        return this.loadImported<T[]>(r, options)
    }

    async addRawSingle<T extends ImportResult | undefined = ImportResult | undefined>(res: T, options: AddRawOptions = {}): Promise<T | undefined> {
        return (await this.addRaw<T>(res, options))?.[0]
    }

    private _sceneUpdated<T extends keyof ISceneEventMap>(ev: BaseEvent<T> & ISceneEventMap[T]) { // todo: check if objects are added some other way.
        if (ev.type === 'addSceneObject') {
            const event = ev as ISceneEventMap['addSceneObject']
            const target = event.object as ImportResult
            switch (target.assetType) {
            case 'material':
                this.materials.registerMaterial(<IMaterial>target)
                break
            case 'texture':
                break
            case 'model':
            case 'light':
            case 'camera':
                break
            default:
                break
            }
        } else if (ev.type === 'materialChanged') {
            const event = ev as ISceneEventMap['materialChanged']
            const target = event.material as IMaterial | IMaterial[] | undefined
            const targets = Array.isArray(target) ? target : target ? [target] : []
            for (const t of targets) {
                this.materials.registerMaterial(t)
            }
        } else if (ev.type === 'beforeDeserialize') {// todo where is this used? is it needed?
            const event = ev as ISceneEventMap['beforeDeserialize']
            // object/material/texture to be deserialized
            const data = event.data as any
            const meta = event.meta
            if (!data.metadata) {
                console.warn('Invalid data(no metadata)', data)
            }
            if (event.material) {
                if (data.metadata?.type !== 'Material') {
                    console.warn('Invalid material data', data)
                }
                JSONMaterialLoader.DeserializeMaterialJSON(data, this.viewer, meta, event.material).then(() => {
                    //
                })
            }

        } else {
            console.error('Unexpected')
        }
    }

    dispose() {
        this.importer.dispose()
        this.materials.dispose()
        this.processState.clear()
        this.viewer.scene.removeEventListener('addSceneObject', this._sceneUpdated)
        this.viewer.scene.removeEventListener('materialChanged', this._sceneUpdated)
        this.exporter.dispose()
    }

    protected _addImporters() {
        const viewer = this.viewer
        if (!viewer) return
        const importer = this.importer

        // todo fix - loading manager getHandler matches backwards?
        const importers: Importer[] = [
            new Importer(class extends SimpleJSONLoader {
                async parseAsync(json: Record<string, any>): Promise<any> {
                    if (json.assetType === 'config') return json // process later
                    // When a file with .json extension and a .type is imported, we can forward it to other loaders inheriting SimpleJSONLoader that support that type like JSONMaterialLoader
                    // getOrCall ensures we get all materials registered at runtime
                    const type = json.type || json.metadata?.type
                    const imps = importer.importers.filter(i=>{
                        const t = ((i as Importer).cls as typeof SimpleJSONLoader)?.SupportedJSONTypes
                        if (t) return getOrCall(t)?.includes(type)
                    })
                    for (const imp of imps) {
                        if (!imp) continue
                        const loader = imp.ctor(importer) as SimpleJSONLoader
                        const res = await loader.parseAsync(json)
                        ;(loader as ILoader).dispose && (loader as ILoader).dispose!()
                        if (res) return res // loader can return undefined if they don't want to support this file
                    }
                    if (json.serializableClassId) { // todo check json.type also as its supported in Serialization? or just do it anyway for any object because it could have nested serialized objects?
                        const resources = json.resources
                        if (json.resources) delete json.resources
                        const meta = resources ? await viewer.loadConfigResources(resources) : undefined
                        const res = ThreeSerialization.Deserialize(json, undefined, meta)
                        if (meta) json.resources = meta
                        if (res) return res
                    }
                    return super.parseAsync(json)
                }
            }, ['json', 'vjson'], ['application/json'], false),

            new Importer(SVGTextureLoader, ['svg', 'data:image/svg'], ['image/svg+xml'], false), // todo: use ImageBitmapLoader if supported (better performance)

            new Importer(TextureLoader, ['webp', 'png', 'jpeg', 'jpg', 'ico', 'data:image', 'avif', 'bmp', 'gif', 'tiff'], [
                'image/webp', 'image/png', 'image/jpeg', 'image/gif', 'image/bmp', 'image/tiff', 'image/x-icon', 'image/avif',
            ], false), // todo: use ImageBitmapLoader if supported (better performance)

            new Importer<JSONMaterialLoader>(JSONMaterialLoader,
                JSONMaterialLoader.SupportedJSONExtensions,
                [], false, (loader) => {
                    if (loader) loader.viewer = this.viewer
                    return loader
                }),

            new Importer(class extends RGBELoader {
                constructor(manager: LoadingManager) {
                    super(manager)
                    this.setDataType(getTextureDataType(viewer.renderManager.renderer))
                }
            }, ['hdr'], ['image/vnd.radiance'], false),

            new Importer(class extends EXRLoader {
                constructor(manager: LoadingManager) {
                    super(manager)
                    this.setDataType(getTextureDataType(viewer.renderManager.renderer))
                }
            }, ['exr'], ['image/x-exr'], false),

            new Importer(FBXLoader2, ['fbx'], ['model/fbx'], true),
            new Importer(ZipLoader, ['zip', 'glbz', 'gltfz'], ['application/zip', 'model/gltf+zip', 'model/zip'], true), // gltfz and glbz are invented zip files with gltf/glb inside along with resources

            new Importer(OBJLoader2 as any as Class<ILoader>, ['obj'], ['model/obj'], true),
            new Importer(MTLLoader2 as any as Class<ILoader>, ['mtl'], ['model/mtl'], false),

            new Importer<GLTFLoader2>(GLTFLoader2, ['gltf', 'glb', 'data:model/gltf', 'data:model/glb'], ['model/gltf', 'model/gltf+json', 'model/gltf-binary', 'model/glb'], true, (l, _, i) => l?.setup(this.viewer, i.extensions)),

            new Importer(DRACOLoader2, ['drc'], ['model/mesh+draco', 'model/drc'], true),

            new Importer(VideoTextureLoader, ['mp4', 'ogg', 'mov', 'webm', 'data:video'], ['video/mp4', 'video/ogg', 'video/quicktime', 'video/webm'], true),

            new Importer(PolyhavenMaterialGLTFLoader, ['phmatgltf'], [], true),
        ]

        this.importer.addImporter(...importers)

    }

    private _gltfExporter = {
        ext: ['gltf', 'glb'],
        extensions: [] as (typeof GLTFExporter2.ExportExtensions)[number][],
        ctor: (_, exporter) => {
            const ex = new GLTFExporter2()
            // This should be added at the end.
            ex.setup(this.viewer, exporter.extensions)
            return ex
        },
    } satisfies IExporter

    protected _addExporters() {
        const exporters: IExporter[] = [this._gltfExporter]

        this.exporter.addExporter(...exporters)
    }

    protected _setupObjectProcess() {
        this.importer.addEventListener('processRaw', (event) => {
            if (event.data && event.data.isObject3D) {
                const node = event.data as IObject3D
                this._loadObjectDependencies(node)
                return
            }
            const mat = event.data as IMaterial
            if (mat && mat.isMaterial && mat.uuid) {
                this.materials.registerMaterial(mat)
            }
        })

        this.importer.addEventListener('processRawStart', (e)=>processRawStartHook(e, this))
    }

    protected _setupObjectExport() {
        // this.exporter.addEventListener('exportFile', ()=>{
        // })
    }

    /**
     * Load the embedded `rootPath` dependencies within this object
     * @param object
     * @private
     */
    private async _loadObjectDependencies(object: IObject3D) {
        const deps = [] as IObject3D[]
        if (!object.traverseModels) {
            this.viewer.console.error('AssetManager - Object not upgraded, cannot load dependencies', object)
            return
        }
        object.traverseModels && object.traverseModels(m => {
            if (m.userData.rootPathRefresh && !m._rootPathRefreshed && !m._rootPathRefreshing && m.userData.rootPath) {
                deps.push(m)
                return false // to not traverse children
            }
            return true
        }, {visible: false, widgets: true})
        const pms = deps.map(async m => {
            m._rootPathRefreshing = true
            const rootPath = m.userData.rootPath
            if (!rootPath) return null
            const rootPathOptions = m.userData.rootPathOptions
            const res = await this.importer.import(rootPath, {
                ...rootPathOptions,
            })
            if (!res) {
                throw new Error(`Unable to load asset from url - ${rootPath}`)
            }
            return res
        })
        const r = await Promise.allSettled(pms)
        // console.log(r)
        for (let i = 0; i < r.length; i++) {
            const res = r[i]
            const obj = deps[i]
            delete obj._rootPathRefreshing
            obj._rootPathRefreshed = true
            if (res.status === 'rejected') {
                this.viewer.console.error(`ThreeViewer - Failed to load root path for object ${obj.name}`, res.reason)
                continue
            }
            if (res.status !== 'fulfilled') continue
            const models = res.value
            if (!models || !models.length) continue
            const model = models.find(m => m?.isObject3D)
            if (!model) {
                this.viewer.console.warn('AssetManager - No valid model found in root path', res.value)
                continue
            }
            const others = models.filter(m => m && m !== model)
            const parent = obj.parent
            const newIndex = parent ? parent.children.indexOf(obj) : -1
            if (parent) obj.removeFromParent()
            this.viewer.object3dManager.unregisterObject(obj)
            if (!model.isObject3D) {
                this.viewer.console.warn('Non model dependency loaded. Not fully supported yet.')
                // todo?
                continue
            }
            if (!parent) {
                this.viewer.console.error('AssetManager - Unexpected error, no parent found for object when loading dependency', obj)
                // parent = this.viewer.scene.modelRoot
                continue
            }
            if (model._copyFromEmbedded) model._copyFromEmbedded(obj) // todo better name, document this in IObject3D or ImportResult?
            else {
                obj.matrix.decompose(model.position, model.quaternion, model.scale)
                model.name = obj.name // copy name
                model.userData = {...obj.userData, ...model.userData} // merge userData
                model._rootPathRefreshed = true // mark as refreshed
                // @ts-expect-error force update
                model.uuid = obj.uuid
            }
            parent.add(model as IObject3D)
            const newIndex2 = parent.children.indexOf(model as IObject3D)
            if (newIndex >= 0 && newIndex2 >= 0 && newIndex !== newIndex2) {
                parent.children.splice(newIndex2, 1)
                parent.children.splice(newIndex, 0, model as IObject3D) // add at new index
            }
            if (others.length) {
                for (const other of others) {
                    if (other?.isObject3D) {
                        parent.add(other as Object3D)
                    } else {
                        this.viewer.console.warn('Non model dependency loaded. Not fully supported yet.', other)
                    }
                }
            }
        }
    }

    // region process state

    /**
     * State of download/upload/process/other processes in the viewer.
     * Subscribes to importer and exporter by default, more can be added by plugins like {@link FileTransferPlugin}
     */
    processState: Map<string, {state: string, progress?: number | undefined}> = new Map()

    /**
     * Set process state for a path
     * Progress should be a number between 0 and 100
     * Pass undefined in value to remove the state
     * @param path
     * @param value
     */
    setProcessState(path: string, value: {state: string, progress?: number | undefined} | undefined) {
        if (value === undefined) this.processState.delete(path)
        else this.processState.set(path, value)
        this.dispatchEvent({type: 'processStateUpdate'})
    }

    protected _setupProcessState() {
        this.importer.addEventListener('importFile', (data) => {
            if (!data.path || data.path.startsWith('blob:') || data.path.startsWith('data:')) return
            this.setProcessState(data.path, data.state !== 'done' ? {
                state: data.state,
                progress: data.progress ? data.progress * 100 : undefined,
            } : undefined)
        })
        this.importer.addEventListener('processRawStart', (data) => {
            if (!data.path || data.path.startsWith('blob:') || data.path.startsWith('data:')) return
            this.setProcessState(data.path, {
                state: 'processing',
                progress: undefined,
            })
        })
        this.importer.addEventListener('processRaw', (data) => {
            if (!data.path || data.path.startsWith('blob:') || data.path.startsWith('data:')) return
            this.setProcessState(data.path, undefined)
        })
        this.exporter.addEventListener('exportFile', (data) => {
            if (!data.obj.name) return
            this.setProcessState(data.obj.name, data.state !== 'done' ? {
                state: data.state,
                progress: data.progress ? data.progress * 100 : undefined,
            } : undefined)
        })
    }

    // endregion

    // region glTF extensions registration helpers

    gltfExtensions: {
        name: string
        import: (parser: GLTFParser, viewer?: ThreeViewer) => GLTFLoaderPlugin,
        export: (parser: GLTFWriter2) => GLTFExporterPlugin,
        textures?: Record<string, string|number> // see GLTFDracoExportPlugin
    }[] = []

    protected _setupGltfExtensions() {
        this.importer.addEventListener('loaderCreate', this._loaderCreate as any)
        this.viewer.forPlugin('GLTFDracoExportPlugin', (p)=> {
            if (!p.addExtension) return
            for (const gltfExtension of this.gltfExtensions) {
                p.addExtension(gltfExtension.name, gltfExtension.textures)
            }
        })
    }

    protected _loaderCreate({loader}: {loader: GLTFLoader2}) {
        if (!loader.isGLTFLoader2) return
        for (const gltfExtension of this.gltfExtensions) {
            loader.register(gltfExtension.import)
        }
    }

    registerGltfExtension(ext: AssetManager['gltfExtensions'][number]) {
        const ext1 = this.gltfExtensions.findIndex(e => e.name === ext.name)
        if (ext1 >= 0) this.gltfExtensions.splice(ext1, 1)
        this.gltfExtensions.push(ext)
        this._gltfExporter.extensions.push(ext.export)
        const exporter2 = this.exporter.getExporter('gltf', 'glb')
        if (exporter2 && exporter2 !== this._gltfExporter)
            exporter2.extensions?.push(ext.export)
    }

    unregisterGltfExtension(name: string) {
        const ind = this.gltfExtensions.findIndex(e => e.name === name)
        if (ind < 0) return
        this.gltfExtensions.splice(ind, 1)
        const ind1 = this._gltfExporter.extensions.findIndex(e => e.name === name)
        if (ind1 >= 0) this._gltfExporter.extensions.splice(ind1, 1)
        const exporter2 = this.exporter.getExporter('gltf', 'glb')
        if (exporter2?.extensions && exporter2 !== this._gltfExporter) {
            const ind2 = exporter2.extensions.findIndex(e => e.name === name)
            if (ind2 >= 0) exporter2.extensions?.splice(ind2, 1)
        }
    }

    // endregion

    // region deprecated

    /**
     * @deprecated use addRaw instead
     * @param res
     * @param options
     */
    async addImported<T extends (ImportResult | undefined) = ImportResult>(res: T | T[], options: AddRawOptions = {}): Promise<(T | undefined)[]> {
        console.error('addImported is deprecated, use addRaw instead')
        return this.addRaw(res, options)
    }

    /**
     * @deprecated use addAsset instead
     * @param path
     * @param options
     */
    public async addFromPath(path: string, options: ImportAddOptions = {}): Promise<any[]> {
        console.error('addFromPath is deprecated, use addAsset instead')
        return this.addAsset(path, options)
    }

    /**
     * @deprecated use {@link ThreeViewer.exportConfig} instead
     * @param binary - if set to false, encodes all the array buffers to base64
     */
    exportViewerConfig(binary = true): Record<string, any> {
        if (!this.viewer) return {}
        console.error('exportViewerConfig is deprecated, use viewer.toJSON instead')
        return this.viewer.toJSON(binary, undefined)
    }

    /**
     * @deprecated use {@link ThreeViewer.exportPluginsConfig} instead
     * @param filter
     */
    exportPluginPresets(filter?: string[]) {
        console.error('exportPluginPresets is deprecated, use viewer.exportPluginsConfig instead')
        return this.viewer?.exportPluginsConfig(filter)
    }

    /**
     * @deprecated use {@link ThreeViewer.exportPluginConfig} instead
     * @param plugin
     */
    exportPluginPreset(plugin: IViewerPlugin) {
        console.error('exportPluginPreset is deprecated, use viewer.exportPluginConfig instead')
        return this.viewer?.exportPluginConfig(plugin)
    }

    /**
     * @deprecated use {@link ThreeViewer.importPluginConfig} instead
     * @param json
     * @param plugin
     */
    async importPluginPreset(json: any, plugin?: IViewerPlugin) {
        console.error('importPluginPreset is deprecated, use viewer.importPluginConfig instead')
        return this.viewer?.importPluginConfig(json, plugin)
    }

    // todo continue from here by moving functions to the viewer.
    /**
     * @deprecated use {@link ThreeViewer.importConfig} instead
     * @param viewerConfig
     */
    async importViewerConfig(viewerConfig: any) {
        return this.viewer?.importConfig(viewerConfig)
    }

    /**
     * @deprecated use {@link ThreeViewer.fromJSON} instead
     * @param viewerConfig
     */
    applyViewerConfig(viewerConfig: any, resources?: any) {
        console.error('applyViewerConfig is deprecated, use viewer.fromJSON instead')
        return this.viewer?.fromJSON(viewerConfig, resources)
    }

    /**
     * @deprecated moved to {@link ThreeViewer.loadConfigResources}
     * @param json
     * @param extraResources - preloaded resources in the format of viewer config resources.
     */
    async importConfigResources(json: any, extraResources?: any) {
        if (!this.importer) throw 'Importer not initialized yet.'

        if (json.__isLoadedResources) return json

        return this.viewer?.loadConfigResources(json, extraResources)
    }

    /**
     * @deprecated not a plugin anymore
     */
    static readonly PluginType = 'AssetManager'
    // endregion

}

const processRawStartHook = (event: IAssetImporterEventMap['processRawStart'], manager: AssetManager) => {
    // console.log('preprocess mat', mat)
    const res = event.data!
    const options = event.options! as ProcessRawOptions
    // if (!res.assetType) {
    //     if (res.isBufferGeometry) { // for eg stl todo
    //         res = new Mesh(res, new MeshStandardMaterial())
    //     }
    //     if (res.isObject3D) {
    //     }
    // }
    if (res.isObject3D) {
        const cameras: Camera[] = []
        const lights: Light[] = []
        res.traverse((obj: any) => {
            if (obj.material) {
                const materials = Array.isArray(obj.material) ? obj.material : [obj.material]
                const newMaterials = []
                const textures = []
                for (const material of materials) {
                    const mat = manager.materials.convertToIMaterial(material, {createFromTemplate: options.replaceMaterials !== false}) || material
                    mat.uuid = material.uuid
                    mat.userData.uuid = material.uuid
                    newMaterials.push(mat)
                    const maps: Map<string, ITexture> = iMaterialCommons.getMapsForMaterial.call(mat)
                    textures.push(...Array.from(maps.values()))
                }
                if (Array.isArray(obj.material)) obj.material = newMaterials
                else obj.material = newMaterials[0]
                new Set(textures).forEach(t => {
                    if (typeof t.userData.rootPath === 'string' && t.userData.rootPath.startsWith('blob:')) { // because we are not checking when setting inside three.js fork
                        delete t.userData.rootPath
                    }
                    // embedded texture loaded inside some other loader.
                    if (t.userData.rootPath && !(t as ImportResult).__rootPath) {
                        (t as ImportResult).__rootPath = t.userData.rootPath
                        // todo we are ignoring the promise from process raw
                        manager.importer.processRawSingle(t, {})
                    } else {
                        upgradeTexture.call(t)
                    }
                })
            }
            if (obj.isCamera) cameras.push(obj)
            if (obj.isLight) lights.push(obj)
        })
        for (const camera of cameras) {
            if ((camera as Partial<ICamera>).assetType === 'camera') continue
            // todo: OrthographicCamera
            if (!camera.parent || options.replaceCameras === false) {
                iCameraCommons.upgradeCamera.call(camera)
            } else {
                const newCamera: ICamera = (camera as any).iCamera ??
                !(camera as Partial<ICamera>).isOrthographicCamera ?
                    new PerspectiveCamera2('', manager.viewer.canvas) :
                    new OrthographicCamera2('', manager.viewer.canvas)
                if (camera === newCamera) continue
                camera.parent.children.splice(camera.parent.children.indexOf(camera), 1, newCamera)
                newCamera.parent = camera.parent as any
                newCamera.copy(camera as any)
                camera.parent = null
                ;(newCamera as any).uuid = camera.uuid
                newCamera.userData.uuid = camera.uuid
                ;(camera as any).iCamera = newCamera
                // console.log('replacing camera', camera, newCamera)
            }
        }
        for (const light of lights) {
            if ((light as ILight).assetType === 'light') continue
            if (!light.parent || options.replaceLights === false) {
                iLightCommons.upgradeLight.call(light)
            } else {
                const newLight: ILight | undefined = (light as any).iLight ??
                (light as any).isDirectionalLight ? new DirectionalLight2() :
                    (light as any).isPointLight ? new PointLight2() :
                        (light as any).isSpotLight ? new SpotLight2() :
                            (light as any).isAmbientLight ? new AmbientLight2() :
                                (light as any).isHemisphereLight ? new HemisphereLight2() :
                                    (light as any).isRectAreaLight ? new RectAreaLight2() :
                                        undefined
                if (light === newLight || !newLight) continue
                light.parent.children.splice(light.parent.children.indexOf(light), 1, newLight)
                newLight.parent = light.parent as any
                newLight.copy(light as any)
                light.parent = null
                ;(newLight as any).uuid = light.uuid
                newLight.userData.uuid = light.uuid
                ;(light as any).iLight = newLight
            }
        }

        iObjectCommons.upgradeObject3D.call(res)
    } else if (res.isMaterial) {
        if (!res.assetType) iMaterialCommons.upgradeMaterial.call(res)
        // todo update res by generating new material?
    } else if (res.isTexture) {
        upgradeTexture.call(res)

        if (event?.options?.generateMipmaps !== undefined)
            res.generateMipmaps = event?.options.generateMipmaps
        if (!res.generateMipmaps && !res.isRenderTargetTexture) { // todo: do we need to check more?
            res.minFilter = res.minFilter === LinearMipmapLinearFilter ? LinearFilter : res.minFilter
            res.magFilter = res.magFilter === LinearMipmapLinearFilter ? LinearFilter : res.magFilter
        }

    }
    // todo other asset/object types?
}
