import { roundToMilliseconds } from "./shared/PrecisionUtils.js";
import { SampleBuffer } from "../SampleBuffer.js";
import { AudioSampleSink, BufferSource, Input, InputAudioTrack, InputVideoTrack, MP4, VideoSampleSink } from "mediabunny";
const defaultOptions = {
	videoBufferSize: 30,
	audioBufferSize: 100,
	startTimeOffsetMs: 0
};
var NoSample = class extends RangeError {};
var BufferedSeekingInput = class {
	constructor(arrayBuffer, options) {
		this.trackIterators = /* @__PURE__ */ new Map();
		this.trackBuffers = /* @__PURE__ */ new Map();
		this.trackIteratorCreationPromises = /* @__PURE__ */ new Map();
		this.trackSeekPromises = /* @__PURE__ */ new Map();
		const bufferSource = new BufferSource(arrayBuffer);
		const input = new Input({
			source: bufferSource,
			formats: [MP4]
		});
		this.input = input;
		this.options = {
			...defaultOptions,
			...options
		};
		this.startTimeOffsetMs = this.options.startTimeOffsetMs ?? 0;
	}
	getBufferSize(trackId) {
		const buffer = this.trackBuffers.get(trackId);
		return buffer ? buffer.length : 0;
	}
	getBufferContents(trackId) {
		const buffer = this.trackBuffers.get(trackId);
		return buffer ? Object.freeze([...buffer.getContents()]) : [];
	}
	getBufferTimestamps(trackId) {
		const contents = this.getBufferContents(trackId);
		return contents.map((sample) => sample.timestamp || 0);
	}
	clearBuffer(trackId) {
		const buffer = this.trackBuffers.get(trackId);
		if (buffer) buffer.clear();
	}
	computeDuration() {
		return this.input.computeDuration();
	}
	async getTrack(trackId) {
		const tracks = await this.input.getTracks();
		const track = tracks.find((track$1) => track$1.id === trackId);
		if (!track) throw new Error(`Track ${trackId} not found`);
		return track;
	}
	async getAudioTrack(trackId) {
		const tracks = await this.input.getAudioTracks();
		const track = tracks.find((track$1) => track$1.id === trackId && track$1.type === "audio");
		if (!track) throw new Error(`Track ${trackId} not found`);
		return track;
	}
	async getVideoTrack(trackId) {
		const tracks = await this.input.getVideoTracks();
		const track = tracks.find((track$1) => track$1.id === trackId && track$1.type === "video");
		if (!track) throw new Error(`Track ${trackId} not found`);
		return track;
	}
	async getFirstVideoTrack() {
		const tracks = await this.input.getVideoTracks();
		return tracks[0];
	}
	async getFirstAudioTrack() {
		const tracks = await this.input.getAudioTracks();
		return tracks[0];
	}
	getTrackIterator(track) {
		if (this.trackIterators.has(track.id)) return this.trackIterators.get(track.id);
		const trackIterator = this.createTrackIterator(track);
		this.trackIterators.set(track.id, trackIterator);
		return trackIterator;
	}
	createTrackSampleSink(track) {
		if (track instanceof InputAudioTrack) return new AudioSampleSink(track);
		if (track instanceof InputVideoTrack) return new VideoSampleSink(track);
		throw new Error(`Unsupported track type ${track.type}`);
	}
	createTrackIterator(track) {
		const sampleSink = this.createTrackSampleSink(track);
		return sampleSink.samples();
	}
	createTrackBuffer(track) {
		if (track.type === "audio") {
			const bufferSize$1 = this.options.audioBufferSize;
			const sampleBuffer$1 = new SampleBuffer(bufferSize$1);
			return sampleBuffer$1;
		}
		const bufferSize = this.options.videoBufferSize;
		const sampleBuffer = new SampleBuffer(bufferSize);
		return sampleBuffer;
	}
	getTrackBuffer(track) {
		const maybeTrackBuffer = this.trackBuffers.get(track.id);
		if (maybeTrackBuffer) return maybeTrackBuffer;
		const trackBuffer = this.createTrackBuffer(track);
		this.trackBuffers.set(track.id, trackBuffer);
		return trackBuffer;
	}
	async seek(trackId, timeMs) {
		const mediaTimeMs = timeMs + this.startTimeOffsetMs;
		const roundedMediaTimeMs = roundToMilliseconds(mediaTimeMs);
		const existingSeek = this.trackSeekPromises.get(trackId);
		if (existingSeek) await existingSeek;
		const seekPromise = this.seekSafe(trackId, roundedMediaTimeMs);
		this.trackSeekPromises.set(trackId, seekPromise);
		try {
			return await seekPromise;
		} finally {
			this.trackSeekPromises.delete(trackId);
		}
	}
	async resetIterator(track) {
		const trackBuffer = this.trackBuffers.get(track.id);
		trackBuffer?.clear();
		const ongoingIteratorCreation = this.trackIteratorCreationPromises.get(track.id);
		if (ongoingIteratorCreation) await ongoingIteratorCreation;
		const iterator = this.trackIterators.get(track.id);
		if (iterator) try {
			await iterator.return?.();
		} catch (_error) {}
		this.trackIterators.delete(track.id);
	}
	#seekLock;
	async seekSafe(trackId, timeMs) {
		if (this.#seekLock) await this.#seekLock.promise;
		const seekLock = Promise.withResolvers();
		this.#seekLock = seekLock;
		try {
			const track = await this.getTrack(trackId);
			const trackBuffer = this.getTrackBuffer(track);
			const roundedTimeMs = roundToMilliseconds(timeMs);
			const firstTimestampMs = roundToMilliseconds(await track.getFirstTimestamp() * 1e3);
			if (roundedTimeMs < firstTimestampMs) {
				console.error("Seeking outside bounds of input", {
					roundedTimeMs,
					firstTimestampMs
				});
				throw new NoSample(`Seeking outside bounds of input ${roundedTimeMs} < ${firstTimestampMs}`);
			}
			const bufferContents = trackBuffer.getContents();
			if (bufferContents.length > 0) {
				const bufferStartMs = roundToMilliseconds(trackBuffer.firstTimestamp * 1e3);
				if (roundedTimeMs < bufferStartMs) await this.resetIterator(track);
			}
			const alreadyInBuffer = trackBuffer.find(timeMs);
			if (alreadyInBuffer) return alreadyInBuffer;
			const iterator = this.getTrackIterator(track);
			while (true) {
				const { done, value: decodedSample } = await iterator.next();
				if (decodedSample) trackBuffer.push(decodedSample);
				const foundSample = trackBuffer.find(roundedTimeMs);
				if (foundSample) return foundSample;
				if (done) break;
			}
			const finalBufferContents = trackBuffer.getContents();
			if (finalBufferContents.length > 0) {
				const lastSample = finalBufferContents[finalBufferContents.length - 1];
				const lastSampleEndMs = roundToMilliseconds(((lastSample?.timestamp || 0) + (lastSample?.duration || 0)) * 1e3);
				const trackDurationMs = await track.computeDuration() * 1e3;
				const isSeekingToTrackEnd = roundToMilliseconds(timeMs) === roundToMilliseconds(trackDurationMs);
				const isAtEndOfTrack = roundToMilliseconds(timeMs) >= lastSampleEndMs;
				if (isSeekingToTrackEnd && isAtEndOfTrack) return lastSample;
			}
			throw new NoSample(`Sample not found for time ${timeMs} in ${track.type} track ${trackId}`);
		} finally {
			this.#seekLock = void 0;
			seekLock.resolve();
		}
	}
};
export { BufferedSeekingInput };
