import { AssetCache } from "./AssetCache.js";
import { AssetDependencyGraph } from "./AssetDependencyGraph.js";
import { AssetHandle } from "./AssetHandle.js";
import { AssetLoadError } from "./AssetLoader.js";
import { AssetRegistry } from "./AssetRegistry.js";
import { LoadContext } from "./LoadContext.js";
export class AssetManager {
    registry = new AssetRegistry();
    cache = new AssetCache();
    dependencies = new AssetDependencyGraph();
    baseUrl;
    defaultRetries;
    defaultRetryDelayMs;
    nextId = 1;
    constructor(options = {}) {
        this.baseUrl = options.baseUrl ?? "";
        this.defaultRetries = Math.max(0, Math.trunc(options.retries ?? 0));
        this.defaultRetryDelayMs = Math.max(0, options.retryDelayMs ?? 0);
    }
    register(loader) {
        this.registry.register(loader);
    }
    async load(url, options = {}) {
        const context = new LoadContext({
            baseUrl: this.baseUrl,
            signal: options.signal,
            dependencyChain: options.dependencyChain,
            manager: this
        });
        const resolvedUrl = context.resolve(url);
        context.throwIfAborted(resolvedUrl);
        const key = `${options.type ?? "auto"}:${resolvedUrl}`;
        const cached = this.cache.get(key);
        if (cached) {
            return cached.retain();
        }
        const inFlight = this.cache.getInFlight(key);
        if (inFlight) {
            const handle = await this.awaitInFlight(inFlight, context, resolvedUrl);
            return handle.retain();
        }
        const request = { url: resolvedUrl, type: options.type, signal: options.signal, onProgress: options.onProgress };
        const loader = this.registry.find(request);
        const promise = this.loadWithLoader(loader, request, context.child(resolvedUrl), {
            retries: Math.max(0, Math.trunc(options.retries ?? this.defaultRetries)),
            retryDelayMs: Math.max(0, options.retryDelayMs ?? this.defaultRetryDelayMs)
        });
        this.cache.setInFlight(key, promise);
        return promise;
    }
    async release(handle) {
        const remaining = await handle.release();
        if (remaining > 0) {
            return;
        }
        for (const dependencyId of this.dependencies.dependenciesOf(handle.id)) {
            const dependencyKey = this.cache.keys().find((key) => this.cache.get(key)?.id === dependencyId);
            const dependency = dependencyKey ? this.cache.get(dependencyKey) : undefined;
            if (dependency && !dependency.disposed) {
                await this.release(dependency);
            }
        }
        const cacheKey = this.cache.keys().find((key) => this.cache.get(key)?.id === handle.id);
        if (cacheKey) {
            this.cache.delete(cacheKey);
        }
        this.dependencies.remove(handle.id);
    }
    async loadWithLoader(loader, request, context, retry) {
        let attempt = 0;
        while (true) {
            context.throwIfAborted(request.url);
            const dependencyHandles = [];
            try {
                const dependencyUrls = await loader.dependencies?.(request, context);
                for (const dependencyUrl of dependencyUrls ?? []) {
                    context.throwIfAborted(request.url);
                    dependencyHandles.push(await context.loadDependency(dependencyUrl));
                }
                context.throwIfAborted(request.url);
                const value = (await loader.load(request, context));
                const handle = new AssetHandle({
                    id: `asset-${this.nextId++}`,
                    url: request.url,
                    type: loader.type,
                    value,
                    dispose: (asset) => loader.dispose?.(asset)
                });
                this.dependencies.addNode(handle.id);
                for (const dependency of dependencyHandles) {
                    this.dependencies.addDependency(handle.id, dependency.id);
                }
                return handle;
            }
            catch (error) {
                await this.releaseLoadedDependencies(dependencyHandles);
                if (this.shouldRetry(error, context, attempt, retry.retries)) {
                    attempt += 1;
                    await this.waitForRetry(retry.retryDelayMs, context, request.url);
                    continue;
                }
                if (error instanceof AssetLoadError) {
                    throw error;
                }
                throw new AssetLoadError(`Failed to load asset ${request.url}`, request.url, context.dependencyChain, error);
            }
        }
    }
    async releaseLoadedDependencies(dependencyHandles) {
        for (const dependency of [...dependencyHandles].reverse()) {
            if (!dependency.disposed) {
                await this.release(dependency);
            }
        }
    }
    async awaitInFlight(promise, context, url) {
        if (!context.signal) {
            return promise;
        }
        context.throwIfAborted(url);
        return new Promise((resolve, reject) => {
            const abort = () => {
                reject(new AssetLoadError("Asset load aborted", url, context.dependencyChain, context.signal?.reason));
            };
            context.signal?.addEventListener("abort", abort, { once: true });
            promise.then((handle) => {
                context.signal?.removeEventListener("abort", abort);
                resolve(handle);
            }, (error) => {
                context.signal?.removeEventListener("abort", abort);
                reject(error);
            });
        });
    }
    shouldRetry(error, context, attempt, retries) {
        if (attempt >= retries)
            return false;
        if (context.signal?.aborted)
            return false;
        if (error instanceof AssetLoadError && error.message === "Asset load aborted")
            return false;
        return true;
    }
    async waitForRetry(delayMs, context, url) {
        context.throwIfAborted(url);
        if (delayMs === 0)
            return;
        await new Promise((resolve, reject) => {
            const timeout = setTimeout(() => {
                context.signal?.removeEventListener("abort", abort);
                resolve();
            }, delayMs);
            const abort = () => {
                clearTimeout(timeout);
                reject(new AssetLoadError("Asset load aborted", url, context.dependencyChain, context.signal?.reason));
            };
            context.signal?.addEventListener("abort", abort, { once: true });
        });
    }
}
//# sourceMappingURL=AssetManager.js.map