import { Constants } from "@babylonjs/core/Engines/constants";
import { Texture } from "@babylonjs/core/Materials/Textures/texture";
import { Observable } from "@babylonjs/core/Misc/observable";
import { TimingTools } from "@babylonjs/core/Misc/timingTools";
import { SharedToonTextures } from "./sharedToonTextures";
import { pathNormalize } from "./Util/pathNormalize";
class TextureLoadingModel {
    uniqueId;
    leftLoadCount;
    isRequesting;
    errorTextureDatas;
    constructor(uniqueId) {
        this.uniqueId = uniqueId;
        this.leftLoadCount = 0;
        this.isRequesting = true;
        this.errorTextureDatas = [];
    }
}
class TextureLoadInfo {
    observable;
    hasLoadError;
    constructor() {
        this.observable = new Observable();
        this.hasLoadError = false;
    }
}
class MmdTextureData {
    cacheKey;
    _scene;
    _assetContainer;
    _options;
    _onLoad;
    _onError;
    _texture;
    constructor(cacheKey, scene, assetContainer, urlOrTextureName, useLazyLoadWithBuffer, options, onLoad, onError) {
        this.cacheKey = cacheKey;
        this._scene = scene;
        this._assetContainer = assetContainer;
        this._options = options;
        this._onLoad = onLoad;
        this._onError = onError;
        this._texture = null;
        if (!useLazyLoadWithBuffer) {
            scene._loadFile(urlOrTextureName, (data) => {
                this._createTexture(scene, assetContainer, cacheKey, data, options, onLoad, (message, exception) => {
                    onError?.(message, exception);
                });
            }, undefined, true, true, (request, exception) => {
                onError?.(request ? request.status + " " + request.statusText : "", exception);
            });
        }
    }
    loadFromArrayBuffer(arrayBuffer) {
        this._createTexture(this._scene, this._assetContainer, this.cacheKey, arrayBuffer, this._options, this._onLoad, (message, exception) => {
            this._onError?.(message, exception);
        });
    }
    _onDisposeCallback = null;
    registerOnDisposeCallback(callback) {
        // dead code. for optimization we don't need to check this
        // if (this._onDisposeCallback !== null) throw new Error("On dispose callback is already registered");
        this._onDisposeCallback = callback;
        this._texture.onDisposeObservable.addOnce(callback);
    }
    unregisterOnDisposeCallback() {
        const callback = this._onDisposeCallback;
        if (callback === null)
            return null;
        this._onDisposeCallback = null;
        this._texture.onDisposeObservable.removeCallback(callback);
        return callback;
    }
    _createTexture(scene, assetContainer, textureName, buffer, options, onLoad, onError) {
        scene._blockEntityCollection = !!assetContainer;
        const textureCreationOptions = {
            noMipmap: options.noMipmap,
            invertY: options.invertY,
            samplingMode: options.samplingMode,
            onLoad: () => {
                if (this._texture === null) {
                    if (onLoad !== null)
                        TimingTools.SetImmediate(onLoad);
                }
                else {
                    onLoad?.();
                }
            },
            onError,
            buffer,
            deleteBuffer: options.deleteBuffer,
            format: options.format,
            mimeType: options.mimeType
        };
        const texture = this._texture = new Texture("data:" + textureName, scene, textureCreationOptions);
        texture._parentContainer = assetContainer;
        scene._blockEntityCollection = false;
        assetContainer?.textures.push(texture);
        texture.name = textureName;
    }
    get texture() {
        return this._texture;
    }
}
/**
 * MMD async texture loader
 *
 * The MMD async texture loader caches redundant textures across multiple materials and simplifies asynchronous textural loading
 *
 * It also includes handling when multiple models are loaded at the same time
 */
export class MmdAsyncTextureLoader {
    /**
     * Observable which is notified when all textures of the model are loaded
     *
     * key: uniqueId, value: Observable<void>
     */
    onModelTextureLoadedObservable = new Map();
    /**
     * Texture cache
     *
     * This cache is used to avoid loading the same texture multiple times
     *
     * Once loaded, all textures are stored in the cache and deleted from the cache on their own when the texture is disposed
     *
     * key: requestString, value: texture
     */
    textureCache = new Map();
    _textureLoadInfoMap = new Map(); // key: requestString
    _loadingModels = new Map(); // key: uniqueId
    _errorTexturesReferenceCount = new Map(); // key: textureName, value: referenceModelCount
    _incrementLeftLoadCount(uniqueId) {
        let model = this._loadingModels.get(uniqueId);
        if (model === undefined) {
            model = new TextureLoadingModel(uniqueId);
            this._loadingModels.set(uniqueId, model);
        }
        model.leftLoadCount += 1;
        let observable = this.onModelTextureLoadedObservable.get(uniqueId);
        if (observable === undefined) {
            observable = new Observable();
            this.onModelTextureLoadedObservable.set(uniqueId, observable);
        }
        return model;
    }
    _decrementLeftLoadCount(model) {
        model.leftLoadCount -= 1;
        if (!model.isRequesting && model.leftLoadCount === 0) {
            this._removeErrorTexturesReferenceCount(model.uniqueId);
            this._loadingModels.delete(model.uniqueId);
            const observable = this.onModelTextureLoadedObservable.get(model.uniqueId);
            observable?.notifyObservers();
            observable?.clear();
            this.onModelTextureLoadedObservable.delete(model.uniqueId);
        }
    }
    /**
     * Notify that the model texture load request has been ended
     *
     * If all the textures are cached, no event callback is called
     *
     * so this method must be called and specified at the end of the model's text load request to handle such an edge case
     *
     * @param uniqueId Model unique id
     */
    loadModelTexturesEnd(uniqueId) {
        const model = this._loadingModels.get(uniqueId);
        if (model === undefined)
            return;
        model.isRequesting = false;
        if (model.leftLoadCount === 0) {
            this._removeErrorTexturesReferenceCount(uniqueId);
            this._loadingModels.delete(uniqueId);
            const observable = this.onModelTextureLoadedObservable.get(uniqueId);
            observable?.notifyObservers();
            observable?.clear();
            this.onModelTextureLoadedObservable.delete(uniqueId);
        }
    }
    _addErrorTextureReferenceCount(uniqueId, textureData) {
        const model = this._loadingModels.get(uniqueId);
        model.errorTextureDatas.push(textureData);
        this._errorTexturesReferenceCount.set(textureData, (this._errorTexturesReferenceCount.get(textureData) ?? 0) + 1);
    }
    _removeErrorTexturesReferenceCount(uniqueId) {
        const model = this._loadingModels.get(uniqueId);
        for (let i = 0; i < model.errorTextureDatas.length; ++i) {
            const textureData = model.errorTextureDatas[i];
            const referenceCount = this._errorTexturesReferenceCount.get(textureData) - 1;
            if (referenceCount === 0) {
                if (textureData.texture !== null) {
                    textureData.texture.dispose();
                }
                else {
                    this._textureLoadInfoMap.delete(textureData.cacheKey);
                    this.textureCache.delete(textureData.cacheKey);
                    this._errorTexturesReferenceCount.delete(textureData);
                }
            }
            else {
                this._errorTexturesReferenceCount.set(textureData, referenceCount);
            }
        }
    }
    _handleTextureOnDispose(textureData) {
        textureData.registerOnDisposeCallback(() => {
            this._textureLoadInfoMap.delete(textureData.cacheKey);
            this.textureCache.delete(textureData.cacheKey);
            this._errorTexturesReferenceCount.delete(textureData);
        });
    }
    _createTextureCacheKey(urlOrTextureName, options) {
        const extensionIndex = urlOrTextureName.lastIndexOf(".");
        let extension = "";
        if (extensionIndex !== -1) {
            extension = urlOrTextureName.substring(extensionIndex);
            urlOrTextureName = urlOrTextureName.substring(0, extensionIndex);
        }
        return urlOrTextureName +
            +(options.noMipmap ?? false) +
            +(options.invertY ?? true) +
            (options.samplingMode ?? Texture.TRILINEAR_SAMPLINGMODE) +
            (options.format ?? Constants.TEXTUREFORMAT_RGBA) +
            extension;
    }
    async _loadTextureAsyncInternal(uniqueId, urlOrTextureName, arrayBufferOrBlob, sharedTextureIndex, scene, assetContainer, options) {
        const model = this._incrementLeftLoadCount(uniqueId);
        const cacheKey = this._createTextureCacheKey(urlOrTextureName, options);
        let textureLoadInfo = this._textureLoadInfoMap.get(cacheKey);
        if (textureLoadInfo === undefined) {
            textureLoadInfo = new TextureLoadInfo();
            this._textureLoadInfoMap.set(cacheKey, textureLoadInfo);
        }
        let textureData = this.textureCache.get(cacheKey);
        if (textureData === undefined && !textureLoadInfo.hasLoadError) {
            const blobOrUrl = sharedTextureIndex !== null ? SharedToonTextures.Data[sharedTextureIndex]
                : urlOrTextureName;
            textureData = new MmdTextureData(cacheKey, scene, assetContainer, blobOrUrl, arrayBufferOrBlob !== null, options, () => {
                this._handleTextureOnDispose(textureData);
                textureLoadInfo.hasLoadError = false;
                textureLoadInfo.observable.notifyObservers(false);
                textureLoadInfo.observable.clear();
            }, (_message, _exception) => {
                if (textureData.texture !== null)
                    this._handleTextureOnDispose(textureData);
                this._addErrorTextureReferenceCount(uniqueId, textureData);
                textureLoadInfo.hasLoadError = true;
                textureLoadInfo.observable.notifyObservers(true);
                textureLoadInfo.observable.clear();
            });
            this.textureCache.set(cacheKey, textureData);
            const arrayBuffer = arrayBufferOrBlob instanceof Blob
                ? await arrayBufferOrBlob.arrayBuffer()
                : arrayBufferOrBlob;
            if (arrayBuffer !== null) {
                textureData.loadFromArrayBuffer(arrayBuffer);
            }
        }
        if (textureData.texture !== null && textureData.texture.isReady()) {
            this._decrementLeftLoadCount(model);
            if (textureLoadInfo.hasLoadError)
                return null;
            return textureData.texture;
        }
        return new Promise((resolve) => {
            textureLoadInfo.observable.addOnce((hasLoadError) => {
                this._decrementLeftLoadCount(model);
                resolve(hasLoadError ? null : textureData.texture);
            });
        });
    }
    /**
     * Load texture asynchronously
     *
     * All texture requests for one model must be executed within one synchronization routine without await
     *
     * Because the internally used left texture count mechanism requires knowing the total number of textures required at the time the request is processed
     *
     * @param uniqueId Model unique id
     * @param rootUrl Root url
     * @param relativeTexturePathOrIndex Relative texture path or shared toon texture index
     * @param scene Scene
     * @param assetContainer Asset container
     * @param options Texture load options
     * @returns Texture
     */
    async loadTextureAsync(uniqueId, rootUrl, relativeTexturePathOrIndex, scene, assetContainer, options) {
        let isSharedToonTexture;
        if (typeof relativeTexturePathOrIndex === "number") {
            if (relativeTexturePathOrIndex < -1 || 9 < relativeTexturePathOrIndex) { // max shared toon texture index is 9. -1 is for error texture
                relativeTexturePathOrIndex = -1;
            }
            relativeTexturePathOrIndex += 1; // remap to 0-10
            isSharedToonTexture = true;
        }
        else {
            isSharedToonTexture = false;
        }
        const finalRelativeTexturePath = isSharedToonTexture
            ? "file:shared_toon_texture_" + relativeTexturePathOrIndex
            : relativeTexturePathOrIndex;
        const requestString = isSharedToonTexture
            ? finalRelativeTexturePath
            : pathNormalize(rootUrl + relativeTexturePathOrIndex);
        return await this._loadTextureAsyncInternal(uniqueId, requestString, null, isSharedToonTexture ? relativeTexturePathOrIndex : null, scene, assetContainer, options);
    }
    /**
     * Load texture from buffer asynchronously
     *
     * All texture requests for one model must be executed within one synchronization routine without await
     *
     * Because the internally used left texture count mechanism requires knowing the total number of textures required at the time the request is processed
     *
     * @param uniqueId Model unique id
     * @param textureName Texture name
     * @param arrayBufferOrBlob Texture data encoded in PNG/JPG/BMP
     * @param scene Scene
     * @param assetContainer Asset container
     * @param options Texture load options
     * @param applyPathNormalization Whether to apply path normalization to the texture name (default: true)
     * @returns Texture
     */
    async loadTextureFromBufferAsync(uniqueId, textureName, arrayBufferOrBlob, scene, assetContainer, options, applyPathNormalization = true) {
        if (applyPathNormalization) {
            textureName = pathNormalize(textureName);
        }
        return await this._loadTextureAsyncInternal(uniqueId, textureName, arrayBufferOrBlob, null, scene, assetContainer, options);
    }
}
