import { ByteArrayInput, Progress, UUID } from "@opendaw/lib-std";
import { DefaultSampleLoader } from "./DefaultSampleLoader";
import { SampleStorage } from "./SampleStorage";
import { SamplePeaks } from "@opendaw/lib-fusion";
import { Workers } from "../Workers";
import { Promises } from "@opendaw/lib-runtime";
export class GlobalSampleLoaderManager {
    #provider;
    #loaders;
    #refCounts;
    #cache;
    #pending;
    constructor(provider) {
        this.#provider = provider;
        this.#loaders = UUID.newSet(({ uuid }) => uuid);
        this.#refCounts = UUID.newSet(({ uuid }) => uuid);
        this.#cache = UUID.newSet(({ uuid }) => uuid);
        this.#pending = UUID.newSet(({ uuid }) => uuid);
    }
    fetch(uuid, progress) {
        return this.#provider.fetch(uuid, progress);
    }
    remove(uuid) {
        this.#refCounts.removeByKeyIfExist(uuid);
        this.#loaders.removeByKeyIfExist(uuid);
        this.#cache.removeByKeyIfExist(uuid);
        this.#pending.removeByKeyIfExist(uuid);
    }
    invalidate(uuid) {
        this.#cache.removeByKeyIfExist(uuid);
        this.#pending.removeByKeyIfExist(uuid);
        this.#loaders.opt(uuid).ifSome(loader => {
            loader.invalidate();
            if (loader instanceof DefaultSampleLoader) {
                this.#load(loader);
            }
        });
    }
    register(uuid) {
        const current = this.#refCounts.opt(uuid);
        if (current.nonEmpty()) {
            current.unwrap().count++;
        }
        else {
            this.#refCounts.add({ uuid, count: 1 });
        }
        return {
            terminate: () => {
                const ref = this.#refCounts.opt(uuid);
                if (ref.isEmpty()) {
                    return;
                }
                const { count } = ref.unwrap();
                if (count <= 1) {
                    this.#refCounts.removeByKey(uuid);
                    this.#loaders.removeByKeyIfExist(uuid);
                    this.#cache.removeByKeyIfExist(uuid);
                }
                else {
                    ref.unwrap().count--;
                }
            }
        };
    }
    record(loader) {
        this.#loaders.add(loader);
    }
    getOrCreate(uuid) {
        return this.#loaders.getOrCreate(uuid, uuid => {
            const loader = new DefaultSampleLoader(uuid);
            this.#load(loader);
            return loader;
        });
    }
    async getAudioData(uuid) {
        const { promise, resolve, reject } = Promise.withResolvers();
        const loader = this.getOrCreate(uuid);
        let subscription;
        subscription = loader.subscribe(state => {
            if (state.type === "error") {
                queueMicrotask(() => subscription.terminate());
                reject(new Error(state.reason));
            }
            else if (loader.data.nonEmpty()) {
                queueMicrotask(() => subscription.terminate());
                resolve(loader.data.unwrap());
            }
        });
        return promise;
    }
    #load(loader) {
        const { uuid } = loader;
        const cached = this.#cache.opt(uuid);
        if (cached.nonEmpty()) {
            const { data, peaks, meta } = cached.unwrap();
            loader.setLoaded(data, peaks, meta);
            return;
        }
        const pending = this.#pending.opt(uuid);
        if (pending.nonEmpty()) {
            pending.unwrap().promise.then(() => {
                const cached = this.#cache.opt(uuid);
                if (cached.nonEmpty()) {
                    const { data, peaks, meta } = cached.unwrap();
                    loader.setLoaded(data, peaks, meta);
                }
            });
            return;
        }
        const promise = SampleStorage.get().load(uuid).then(([data, peaks, meta]) => {
            this.#pending.removeByKey(uuid);
            this.#cache.add({ uuid, data, peaks, meta });
            loader.setLoaded(data, peaks, meta);
        }, () => this.#fetchFromApi(loader).finally(() => this.#pending.removeByKey(uuid))).catch((error) => {
            this.#pending.removeByKey(uuid);
            console.warn("Unexpected error loading sample:", error);
            loader.setError(error instanceof Error ? error.message : String(error));
        });
        this.#pending.add({ uuid, promise });
    }
    async #fetchFromApi(loader) {
        const { uuid } = loader;
        const [fetchProgress, peakProgress] = Progress.split(progress => loader.setProgress(0.1 + 0.9 * progress), 2);
        const fetchResult = await Promises.tryCatch(this.#provider.fetch(uuid, fetchProgress));
        if (fetchResult.status === "rejected") {
            const error = fetchResult.error;
            console.warn(error);
            loader.setError(error instanceof Error ? error.message : String(error));
            return;
        }
        const [audio, meta] = fetchResult.value;
        const shifts = SamplePeaks.findBestFit(audio.numberOfFrames);
        const peaksBuffer = await Workers.Peak.generateAsync(peakProgress, shifts, audio.frames, audio.numberOfFrames, audio.numberOfChannels);
        const storeResult = await Promises.tryCatch(SampleStorage.get().save({ uuid, audio, peaks: peaksBuffer, meta }));
        if (storeResult.status === "resolved") {
            const peaks = SamplePeaks.from(new ByteArrayInput(peaksBuffer));
            this.#cache.add({ uuid, data: audio, peaks, meta });
            loader.setLoaded(audio, peaks, meta);
        }
        else {
            const error = storeResult.error;
            console.warn(error);
            loader.setError(error instanceof Error ? error.message : String(error));
        }
    }
}
