import { currentTimeContext } from "./currentTimeContext.js";
import { durationContext } from "./durationContext.js";
import { loopContext, playingContext } from "./playingContext.js";
import { updateAnimations } from "../elements/updateAnimations.js";
import { ContextProvider } from "@lit/context";

//#region src/gui/PlaybackController.ts
/**
* Manages playback state and audio-driven timing for root temporal elements
*
* Created automatically when a temporal element becomes a root (no parent timegroup)
* Provides playback contexts (playing, loop, currentTimeMs, durationMs) to descendants
* Handles:
* - Audio-driven playback with Web Audio API
* - Seek and frame rendering throttling
* - Time state management with pending seek handling
* - Playback loop behavior
*
* Works with any temporal element (timegroups or standalone media) via PlaybackHost interface
*/
var PlaybackController = class {
	#host;
	#playing = false;
	#loop = false;
	#listeners = /* @__PURE__ */ new Set();
	#playingProvider;
	#loopProvider;
	#currentTimeMsProvider;
	#durationMsProvider;
	#FPS = 30;
	#MS_PER_FRAME = 1e3 / this.#FPS;
	#playbackAudioContext = null;
	#playbackAnimationFrameRequest = null;
	#pendingAudioContext = null;
	#AUDIO_PLAYBACK_SLICE_MS = 47 * 1024 / 48e3 * 1e3;
	#currentTime = void 0;
	#seekInProgress = false;
	#pendingSeekTime;
	#processingPendingSeek = false;
	#loopingPlayback = false;
	#playbackWrapTimeSeconds = 0;
	#seekAbortController = null;
	constructor(host) {
		this.#host = host;
		host.addController(this);
		this.#playingProvider = new ContextProvider(host, {
			context: playingContext,
			initialValue: this.#playing
		});
		this.#loopProvider = new ContextProvider(host, {
			context: loopContext,
			initialValue: this.#loop
		});
		this.#currentTimeMsProvider = new ContextProvider(host, {
			context: currentTimeContext,
			initialValue: host.currentTimeMs
		});
		this.#durationMsProvider = new ContextProvider(host, {
			context: durationContext,
			initialValue: host.durationMs
		});
	}
	get currentTime() {
		const rawTime = this.#currentTime ?? 0;
		const fps = this.#host.fps ?? 30;
		if (!fps || fps <= 0) return rawTime;
		const frameDurationS = 1 / fps;
		const quantizedTime = Math.round(rawTime / frameDurationS) * frameDurationS;
		const durationS = this.#host.durationMs / 1e3;
		return Math.max(0, Math.min(quantizedTime, durationS));
	}
	set currentTime(time) {
		time = Math.max(0, Math.min(this.#host.durationMs / 1e3, time));
		if (Number.isNaN(time)) return;
		if (time === this.#currentTime && !this.#processingPendingSeek) return;
		if (this.#pendingSeekTime === time) return;
		if (this.#seekInProgress) {
			this.#pendingSeekTime = time;
			this.#currentTime = time;
			return;
		}
		this.#currentTime = time;
		this.#seekInProgress = true;
		this.#runSeek(time).finally(async () => {
			const { updateAnimations: updateAnimations$1 } = await import("../elements/updateAnimations.js");
			updateAnimations$1(this.#host);
			if (this.#pendingSeekTime !== void 0 && this.#pendingSeekTime !== time) {
				const pendingTime = this.#pendingSeekTime;
				this.#pendingSeekTime = void 0;
				this.#processingPendingSeek = true;
				try {
					this.currentTime = pendingTime;
				} finally {
					this.#processingPendingSeek = false;
				}
			} else this.#pendingSeekTime = void 0;
		});
	}
	async #runSeek(targetTime) {
		this.#seekAbortController?.abort();
		this.#seekAbortController = new AbortController();
		const signal = this.#seekAbortController.signal;
		try {
			signal.throwIfAborted();
			await this.#host.waitForMediaDurations?.(signal);
			signal.throwIfAborted();
			const newTime = Math.max(0, Math.min(targetTime, this.#host.durationMs / 1e3));
			this.#currentTime = newTime;
			this.#host.requestUpdate("currentTime");
			this.#currentTimeMsProvider.setValue(this.currentTimeMs);
			this.#notifyListeners({
				property: "currentTimeMs",
				value: this.currentTimeMs
			});
			signal.throwIfAborted();
			await this.runThrottledFrameTask();
			signal.throwIfAborted();
			if (!(this.#host.isRestoringFromLocalStorage?.() ?? false)) this.#host.saveTimeToLocalStorage?.(newTime);
			else this.#host.setRestoringFromLocalStorage?.(false);
			this.#seekInProgress = false;
			return newTime;
		} catch (error) {
			if (error instanceof DOMException && error.name === "AbortError") return;
			throw error;
		}
	}
	get playing() {
		return this.#playing;
	}
	setPlaying(value) {
		if (this.#playing === value) return;
		this.#playing = value;
		this.#playingProvider.setValue(value);
		this.#host.requestUpdate("playing");
		this.#notifyListeners({
			property: "playing",
			value
		});
		if (value) this.startPlayback();
		else this.stopPlayback();
	}
	get loop() {
		return this.#loop;
	}
	setLoop(value) {
		if (this.#loop === value) return;
		this.#loop = value;
		this.#loopProvider.setValue(value);
		this.#host.requestUpdate("loop");
		this.#notifyListeners({
			property: "loop",
			value
		});
	}
	get currentTimeMs() {
		return this.currentTime * 1e3;
	}
	setCurrentTimeMs(value) {
		this.currentTime = value / 1e3;
	}
	#updatePlaybackTime(timeMs) {
		const durationMs = this.#host.durationMs;
		const clampedTimeMs = Math.max(0, Math.min(timeMs, durationMs));
		const timeSec = clampedTimeMs / 1e3;
		if (this.#currentTime === timeSec) return;
		this.#currentTime = timeSec;
		this.#host.requestUpdate("currentTime");
		this.#currentTimeMsProvider.setValue(clampedTimeMs);
		this.#notifyListeners({
			property: "currentTimeMs",
			value: clampedTimeMs
		});
		this.runThrottledFrameTask();
	}
	play() {
		this.setPlaying(true);
	}
	pause() {
		this.setPlaying(false);
	}
	#removed = false;
	hostConnected() {
		requestAnimationFrame(() => {
			requestAnimationFrame(() => {
				if (this.#removed || this.#host.playbackController !== this) return;
				if (this.#playing) this.startPlayback();
				else this.#initializeTime();
			});
		});
	}
	async #initializeTime() {
		try {
			const waitPromise = this.#host.waitForMediaDurations?.();
			if (waitPromise) await waitPromise;
		} catch (err) {
			if (!(err instanceof DOMException && err.name === "AbortError" || err instanceof Error && (err.name === "AbortError" || err.message.includes("signal is aborted") || err.message.includes("The user aborted a request")))) console.error("Error in PlaybackController hostConnected:", err);
			return;
		}
		if (this.#removed || this.#host.playbackController !== this) return;
		const maybeLoadedTime = this.#host.loadTimeFromLocalStorage?.();
		if (maybeLoadedTime !== void 0) {
			this.#host.setRestoringFromLocalStorage?.(true);
			this.currentTime = maybeLoadedTime;
		} else if (this.#currentTime === void 0) this.currentTime = 0;
	}
	hostDisconnected() {
		this.pause();
	}
	hostUpdated() {
		this.#durationMsProvider.setValue(this.#host.durationMs);
		this.#currentTimeMsProvider.setValue(this.currentTimeMs);
	}
	#selfRenderAbortController;
	#selfRenderPromise;
	#selfRenderDirty = false;
	#selfRenderSuspended = false;
	suspendSelfRender() {
		this.#selfRenderSuspended = true;
		this.#selfRenderAbortController?.abort();
		this.#selfRenderAbortController = void 0;
	}
	resumeSelfRender() {
		this.#selfRenderSuspended = false;
	}
	/**
	* Run frame rendering via FrameController, or directly on the host if it
	* implements FrameRenderable (standalone media element without a Timegroup).
	*/
	async runThrottledFrameTask() {
		const timeMs = this.currentTimeMs;
		if (this.#host.frameController) {
			try {
				await this.#host.frameController.renderFrame(timeMs, { onAnimationsUpdate: (root) => {
					updateAnimations(root);
				} });
			} catch (error) {
				if (error instanceof DOMException && error.name === "AbortError") return;
				console.error("FrameController error:", error);
			}
			return;
		}
		const host = this.#host;
		if (!host.prepareFrame || !host.renderFrame) return;
		if (this.#selfRenderSuspended) return;
		if (this.#selfRenderPromise) {
			this.#selfRenderDirty = true;
			return this.#selfRenderPromise;
		}
		return this.#startSelfRender(host, timeMs);
	}
	#startSelfRender(host, timeMs) {
		this.#selfRenderAbortController?.abort();
		this.#selfRenderAbortController = new AbortController();
		const signal = this.#selfRenderAbortController.signal;
		this.#selfRenderDirty = false;
		this.#selfRenderPromise = (async () => {
			try {
				await host.prepareFrame(timeMs, signal);
				signal.throwIfAborted();
				host.renderFrame(timeMs);
				updateAnimations(this.#host);
			} catch (error) {
				if (error instanceof DOMException && error.name === "AbortError") return;
				if (error?.name === "AbortError") return;
				console.error("Standalone frame render error:", error);
			} finally {
				this.#selfRenderPromise = void 0;
				if (this.#selfRenderDirty && !this.#selfRenderSuspended) this.#startSelfRender(host, this.currentTimeMs);
			}
		})();
		return this.#selfRenderPromise;
	}
	addListener(listener) {
		this.#listeners.add(listener);
	}
	removeListener(listener) {
		this.#listeners.delete(listener);
	}
	#notifyListeners(event) {
		for (const listener of this.#listeners) listener(event);
	}
	remove() {
		this.#removed = true;
		this.stopPlayback();
		this.#listeners.clear();
		this.#host.removeController(this);
	}
	setPendingAudioContext(context) {
		this.#pendingAudioContext = context;
	}
	#syncPlayheadToAudioContext(startMs) {
		const audioContextTime = this.#playbackAudioContext?.currentTime ?? 0;
		const endMs = this.#host.endTimeMs;
		let rawTimeMs;
		if (this.#playbackWrapTimeSeconds > 0 && audioContextTime >= this.#playbackWrapTimeSeconds) rawTimeMs = (audioContextTime - this.#playbackWrapTimeSeconds) * 1e3 % endMs;
		else {
			rawTimeMs = startMs + audioContextTime * 1e3;
			if (this.#loopingPlayback && rawTimeMs >= endMs) rawTimeMs = rawTimeMs % endMs;
		}
		const nextTimeMs = Math.round(rawTimeMs / this.#MS_PER_FRAME) * this.#MS_PER_FRAME;
		this.#updatePlaybackTime(nextTimeMs);
		if (!this.#loopingPlayback && nextTimeMs >= endMs) {
			this.maybeLoopPlayback();
			return;
		}
		this.#playbackAnimationFrameRequest = requestAnimationFrame(() => {
			this.#syncPlayheadToAudioContext(startMs);
		});
	}
	async maybeLoopPlayback() {
		if (this.#loop) {
			this.setCurrentTimeMs(0);
			requestAnimationFrame(() => {
				this.startPlayback();
			});
		} else {
			this.setCurrentTimeMs(0);
			this.pause();
		}
	}
	async stopPlayback() {
		if (this.#playbackAudioContext) {
			if (this.#playbackAudioContext.state !== "closed") await this.#playbackAudioContext.close();
		}
		if (this.#playbackAnimationFrameRequest) cancelAnimationFrame(this.#playbackAnimationFrameRequest);
		this.#playbackAudioContext = null;
		this.#playbackAnimationFrameRequest = null;
		this.#pendingAudioContext = null;
	}
	async startPlayback() {
		if (this.#removed) return;
		await this.stopPlayback();
		const host = this.#host;
		if (!host) return;
		if (host.waitForMediaDurations) await host.waitForMediaDurations();
		if (this.#removed) return;
		const currentMs = this.currentTimeMs;
		const fromMs = currentMs;
		const toMs = host.endTimeMs;
		if (fromMs >= toMs) {
			this.pause();
			return;
		}
		let bufferCount = 0;
		if (this.#pendingAudioContext) {
			this.#playbackAudioContext = this.#pendingAudioContext;
			this.#pendingAudioContext = null;
		} else this.#playbackAudioContext = new AudioContext({ latencyHint: "playback" });
		this.#loopingPlayback = this.#loop;
		this.#playbackWrapTimeSeconds = 0;
		if (this.#playbackAnimationFrameRequest) cancelAnimationFrame(this.#playbackAnimationFrameRequest);
		this.#syncPlayheadToAudioContext(currentMs);
		const playbackContext = this.#playbackAudioContext;
		if (playbackContext.state === "suspended") try {
			await playbackContext.resume();
			if (playbackContext.state === "suspended") {
				console.warn("AudioContext is suspended and resume() failed. On mobile devices, AudioContext.resume() must be called synchronously within a user interaction handler. Media playback will not work until user has interacted with page.");
				this.setPlaying(false);
				return;
			}
		} catch (error) {
			console.warn("Failed to resume AudioContext:", error, "On mobile devices, AudioContext.resume() must be called synchronously within a user interaction handler.");
			this.setPlaying(false);
			return;
		}
		await playbackContext.suspend();
		let logicalTimeMs = currentMs;
		let audioContextTimeMs = 0;
		let hasWrapped = false;
		const fillBuffer = async () => {
			if (bufferCount > 2) return;
			if (await queueBufferSource()) fillBuffer().catch(() => {});
		};
		const queueBufferSource = async () => {
			if (hasWrapped && !this.#loopingPlayback) return false;
			const startMs = logicalTimeMs;
			const endMs = Math.min(logicalTimeMs + this.#AUDIO_PLAYBACK_SLICE_MS, toMs);
			const willReachEnd = endMs >= toMs;
			if (!host.renderAudio) return false;
			const audioBuffer = await host.renderAudio(startMs, endMs);
			bufferCount++;
			const source = playbackContext.createBufferSource();
			source.buffer = audioBuffer;
			source.connect(playbackContext.destination);
			source.start(audioContextTimeMs / 1e3);
			const sliceDurationMs = endMs - startMs;
			source.onended = () => {
				bufferCount--;
				if (willReachEnd) if (!this.#loopingPlayback) this.maybeLoopPlayback();
				else fillBuffer().catch(() => {});
				else fillBuffer().catch(() => {});
			};
			audioContextTimeMs += sliceDurationMs;
			if (willReachEnd && this.#loopingPlayback) {
				hasWrapped = true;
				this.#playbackWrapTimeSeconds = (toMs - fromMs) / 1e3;
				logicalTimeMs = 0;
			} else logicalTimeMs = endMs;
			return true;
		};
		try {
			await fillBuffer();
			await playbackContext.resume();
		} catch (error) {
			if (error instanceof Error && (error.name === "InvalidStateError" || error.message.includes("closed"))) return;
			throw error;
		}
	}
};

//#endregion
export { PlaybackController };
//# sourceMappingURL=PlaybackController.js.map