import { efContext } from "../gui/efContext.js";
import { withSpan } from "../otel/tracingHelpers.js";
import { __decorate } from "../_virtual/_@oxc-project_runtime@0.95.0/helpers/decorate.js";
import { EFTemporal } from "./EFTemporal.js";
import { isContextMixin } from "../gui/ContextMixin.js";
import { LRUCache } from "../utils/LRUCache.js";
import { createMediaEngineFromSource } from "./EFMedia/MediaEngine.js";
import { UrlGenerator } from "../transcoding/utils/UrlGenerator.js";
import { EFSourceMixin } from "./EFSourceMixin.js";
import { FetchMixin } from "./FetchMixin.js";
import { renderTemporalAudio } from "./renderTemporalAudio.js";
import { EFTargetable } from "./TargetController.js";
import { provide } from "@lit/context";
import { LitElement, css } from "lit";
import { property, state } from "lit/decorators.js";

//#region src/elements/EFMedia.ts
const freqWeightsCache = /* @__PURE__ */ new Map();
/**
* Gets all child elements including slotted content for shadow DOM elements.
* Duplicated here to avoid circular imports from EFTemporal.
*/
const getChildrenIncludingSlotted = (element) => {
	if (element.shadowRoot) {
		const slots = element.shadowRoot.querySelectorAll("slot");
		if (slots.length > 0) {
			const assignedElements = [];
			for (const slot of slots) assignedElements.push(...slot.assignedElements());
			for (const child of element.shadowRoot.children) if (child.tagName !== "SLOT") assignedElements.push(child);
			return assignedElements;
		}
	}
	return Array.from(element.children);
};
const deepGetMediaElements = (element, medias = []) => {
	const children = getChildrenIncludingSlotted(element);
	for (const child of children) if (child instanceof EFMedia) medias.push(child);
	else deepGetMediaElements(child, medias);
	return medias;
};
/**
* Simple async value wrapper that mimics Lit Task interface.
* Used for backwards compatibility with code expecting task-like objects.
*/
var AsyncValue = class {
	#value = void 0;
	#error = void 0;
	#status = "initial";
	#promise = Promise.resolve(void 0);
	#resolvePromise;
	get value() {
		return this.#value;
	}
	get error() {
		return this.#error;
	}
	get status() {
		switch (this.#status) {
			case "initial": return 0;
			case "pending": return 1;
			case "complete": return 2;
			case "error": return 3;
		}
	}
	get taskComplete() {
		return this.#promise;
	}
	/**
	* Set the value (marks status as complete)
	*/
	setValue(value) {
		this.#value = value;
		this.#error = void 0;
		this.#status = "complete";
		this.#resolvePromise?.(value);
	}
	/**
	* Set an error (marks status as error)
	*/
	setError(error) {
		this.#error = error;
		this.#value = void 0;
		this.#status = "error";
		this.#resolvePromise?.(void 0);
	}
	/**
	* Start a new async operation
	*/
	startPending() {
		this.#status = "pending";
		this.#promise = new Promise((resolve) => {
			this.#resolvePromise = resolve;
		});
		this.#promise.catch(() => {});
	}
	/**
	* Run an async function and update status accordingly
	*/
	async run(fn) {
		this.startPending();
		try {
			const result = await fn();
			this.setValue(result);
			return result;
		} catch (error) {
			if (error instanceof Error) this.setError(error);
			else this.setError(new Error(String(error)));
			return;
		}
	}
};
const DECAY_WEIGHT = .8;
function processFFTData(fftData, zeroThresholdPercent = .1) {
	const totalBins = fftData.length;
	const zeroThresholdCount = Math.floor(totalBins * zeroThresholdPercent);
	let zeroCount = 0;
	let cutoffIndex = totalBins;
	for (let i = totalBins - 1; i >= 0; i--) if (fftData[i] ?? true) zeroCount++;
	else if (zeroCount >= zeroThresholdCount) {
		cutoffIndex = i + 1;
		break;
	}
	if (cutoffIndex < zeroThresholdCount) return fftData;
	const resampledData = interpolateData(fftData.slice(0, cutoffIndex), fftData.length);
	const attenuationStartIndex = Math.floor(totalBins * .9);
	for (let i = attenuationStartIndex; i < totalBins; i++) {
		const attenuationProgress = (i - attenuationStartIndex) / (totalBins - attenuationStartIndex) + .2;
		const attenuationFactor = Math.max(0, 1 - attenuationProgress);
		resampledData[i] = Math.floor((resampledData[i] ?? 0) * attenuationFactor);
	}
	return resampledData;
}
function interpolateData(data, targetSize) {
	const resampled = new Uint8Array(targetSize);
	const dataLength = data.length;
	for (let i = 0; i < targetSize; i++) {
		const ratio = i / (targetSize - 1) * (dataLength - 1);
		const index = Math.floor(ratio);
		const fraction = ratio - index;
		if (index >= dataLength - 1) resampled[i] = data[dataLength - 1] ?? 0;
		else resampled[i] = Math.round((data[index] ?? 0) * (1 - fraction) + (data[index + 1] ?? 0) * fraction);
	}
	return resampled;
}
var EFMedia = class extends EFTargetable(EFSourceMixin(EFTemporal(FetchMixin(LitElement)), { assetType: "isobmff_files" })) {
	constructor(..._args) {
		super(..._args);
		this.audioBufferDurationMs = 1e4;
		this.maxAudioBufferFetches = 2;
		this.enableAudioBuffering = true;
		this.mute = false;
		this.fftSize = 128;
		this.fftDecay = 8;
		this.fftGain = 3;
		this.interpolateFrequencies = false;
		this.mediaEngineTask = new AsyncValue();
		this.frequencyDataTask = new AsyncValue();
		this.byteTimeDomainTask = new AsyncValue();
		this.audioSegmentIdTask = new AsyncValue();
		this.audioInitSegmentFetchTask = new AsyncValue();
		this.audioSegmentFetchTask = new AsyncValue();
		this.audioInputTask = new AsyncValue();
		this.audioSeekTask = new AsyncValue();
		this.audioBufferTask = new AsyncValue();
		this.fileId = null;
		this._desiredSeekTimeMs = 0;
	}
	get efContext() {
		return this.rootTimegroup ?? this;
	}
	shouldAutoReady() {
		return false;
	}
	static {
		this.VIDEO_SAMPLE_BUFFER_SIZE = 30;
	}
	static {
		this.AUDIO_SAMPLE_BUFFER_SIZE = 120;
	}
	/**
	* Which tracks this media element requires.
	* Subclasses can override to specify their needs:
	* - "audio" - Only needs audio track (e.g., EFAudio)
	* - "video" - Only needs video track
	* - "both" - Needs both tracks (default for backwards compatibility)
	*
	* This is used during media engine creation to skip validation
	* of tracks that won't be used, avoiding unnecessary network requests.
	*/
	get requiredTracks() {
		return "both";
	}
	static get observedAttributes() {
		return [
			...super.observedAttributes || [],
			"mute",
			"fft-size",
			"fft-decay",
			"fft-gain",
			"interpolate-frequencies",
			"file-id",
			"asset-id",
			"audio-buffer-duration",
			"max-audio-buffer-fetches",
			"enable-audio-buffering",
			"sourcein",
			"sourceout"
		];
	}
	static {
		this.styles = [css`
      :host {
        display: block;
        position: relative;
        overflow: hidden;
      }
    `];
	}
	attributeChangedCallback(name, oldValue, newValue) {
		if (name === "asset-id") {
			this.fileId = newValue;
			return;
		}
		super.attributeChangedCallback(name, oldValue, newValue);
	}
	getFreqWeights() {
		if (freqWeightsCache.has(this.fftSize)) return freqWeightsCache.get(this.fftSize);
		const weights = new Float32Array(this.fftSize / 2).map((_, i) => {
			const frequency = i * 48e3 / this.fftSize;
			if (frequency < 60) return .3;
			if (frequency < 250) return .4;
			if (frequency < 500) return .6;
			if (frequency < 2e3) return .8;
			if (frequency < 4e3) return 1.2;
			if (frequency < 8e3) return 1.6;
			return 2;
		});
		freqWeightsCache.set(this.fftSize, weights);
		return weights;
	}
	getShouldInterpolateFrequencies() {
		return this.interpolateFrequencies;
	}
	getUrlGenerator() {
		return new UrlGenerator(() => this.apiHost ?? "");
	}
	#mediaEngine = void 0;
	#mediaEnginePromise = void 0;
	#mediaEngineError = void 0;
	#mediaEngineSrcKey = null;
	/**
	* Get or create the MediaEngine for this element.
	* Uses caching based on src/fileId to avoid redundant fetches.
	*/
	async getMediaEngine(signal) {
		if (!this.src && !this.fileId) return;
		const srcKey = `${this.src}|${this.fileId}`;
		if (this.#mediaEngineSrcKey === srcKey && this.#mediaEngine) {
			this.setContentReadyState("ready");
			return this.#mediaEngine;
		}
		if (this.#mediaEngineSrcKey === srcKey && this.#mediaEnginePromise) return this.#mediaEnginePromise;
		this.#mediaEngineSrcKey = srcKey;
		this.mediaEngineTask.startPending();
		this.setContentReadyState("loading");
		const loadPromise = this.#loadMediaEngine(signal);
		this.#mediaEnginePromise = loadPromise;
		return loadPromise;
	}
	async #loadMediaEngine(signal) {
		try {
			this.#mediaEngine = await this.#createMediaEngine(signal);
			this.#mediaEngineError = void 0;
			if (this.#mediaEngine) {
				this.mediaEngineTask.setValue(this.#mediaEngine);
				this.#handleMediaEngineComplete();
				this.setContentReadyState("ready");
			} else this.setContentReadyState("idle");
			return this.#mediaEngine;
		} catch (error) {
			this.#mediaEngineError = error instanceof Error ? error : new Error(String(error));
			this.mediaEngineTask.setError(this.#mediaEngineError);
			this.setContentReadyState("error");
			if (!(error instanceof DOMException && error.name === "AbortError" || error instanceof Error && (error.message === "No valid media source" || error.message.includes("File not found") || error.message.includes("404") || error.message.includes("Failed to fetch")))) console.error("Media engine error:", error);
			return;
		}
	}
	async #createMediaEngine(signal) {
		const { src, fileId, apiHost, requiredTracks } = this;
		return createMediaEngineFromSource({
			src,
			fileId,
			apiHost,
			requiredTracks,
			fetchFn: (url, init) => this.fetch(url, init),
			urlGenerator: this.getUrlGenerator(),
			signal
		});
	}
	#handleMediaEngineComplete() {
		this.requestUpdate("intrinsicDurationMs");
		this.requestUpdate("ownCurrentTimeMs");
		if (this.rootTimegroup) queueMicrotask(() => {
			this.rootTimegroup?.requestUpdate("ownCurrentTimeMs");
			this.rootTimegroup?.requestUpdate("durationMs");
		});
	}
	#frequencyDataCache = new LRUCache(100);
	#timeDomainDataCache = new LRUCache(100);
	/**
	* Get frequency data for audio visualization at a given time.
	*/
	async getFrequencyData(timeMs, signal) {
		if (timeMs < 0) return null;
		const cacheKey = `${this.getShouldInterpolateFrequencies()}:${this.fftSize}:${this.fftDecay}:${this.fftGain}:${timeMs}`;
		const cached = this.#frequencyDataCache.get(cacheKey);
		if (cached) return cached;
		try {
			const result = await this.#analyzeFrequencies(timeMs, signal);
			if (result) {
				this.#frequencyDataCache.set(cacheKey, result);
				this.frequencyDataTask.setValue(result);
			}
			return result;
		} catch (error) {
			if (error instanceof DOMException && error.name === "AbortError") throw error;
			return null;
		}
	}
	/**
	* Get time domain data for audio visualization at a given time.
	*/
	async getTimeDomainData(timeMs, signal) {
		if (timeMs < 0) return null;
		const cacheKey = `${this.fftSize}:${timeMs}`;
		const cached = this.#timeDomainDataCache.get(cacheKey);
		if (cached) return cached;
		try {
			const result = await this.#analyzeTimeDomain(timeMs, signal);
			if (result) {
				this.#timeDomainDataCache.set(cacheKey, result);
				this.byteTimeDomainTask.setValue(result);
			}
			return result;
		} catch (error) {
			if (error instanceof DOMException && error.name === "AbortError") throw error;
			return null;
		}
	}
	async #analyzeFrequencies(currentTimeMs, signal) {
		const mediaEngine = await this.getMediaEngine(signal);
		signal?.throwIfAborted();
		if (!mediaEngine?.tracks.audio) return null;
		const frameIntervalMs = 1e3 / 30;
		const earliestFrameMs = currentTimeMs - (this.fftDecay - 1) * frameIntervalMs;
		const fromMs = Math.max(0, earliestFrameMs);
		const maxToMs = currentTimeMs + frameIntervalMs;
		const videoDurationMs = this.intrinsicDurationMs || 0;
		const toMs = videoDurationMs > 0 ? Math.min(maxToMs, videoDurationMs) : maxToMs;
		if (fromMs >= toMs) return null;
		const { fetchAudioSpanningTime: fetchAudioSpan } = await import("./EFMedia/shared/AudioSpanUtils.js");
		let audioSpan;
		try {
			audioSpan = await fetchAudioSpan(this, fromMs, toMs, signal);
		} catch (error) {
			if (error instanceof DOMException && error.name === "AbortError") throw error;
			return null;
		}
		if (!audioSpan?.blob || audioSpan.blob.size < 100) return null;
		const tempAudioContext = new OfflineAudioContext(2, 48e3, 48e3);
		const arrayBuffer = await audioSpan.blob.arrayBuffer();
		signal?.throwIfAborted();
		let audioBuffer;
		try {
			audioBuffer = await tempAudioContext.decodeAudioData(arrayBuffer);
			signal?.throwIfAborted();
		} catch {
			return null;
		}
		const startOffsetMs = audioSpan.startMs;
		const framesData = await Promise.all(Array.from({ length: this.fftDecay }, async (_, i) => {
			const frameOffset = i * (1e3 / 30);
			const startTime = Math.max(0, (currentTimeMs - frameOffset - startOffsetMs) / 1e3);
			const audioContext = new OfflineAudioContext(2, 48e3 / 30, 48e3);
			const analyser = audioContext.createAnalyser();
			analyser.fftSize = this.fftSize;
			analyser.minDecibels = -90;
			analyser.maxDecibels = -10;
			const gainNode = audioContext.createGain();
			gainNode.gain.value = this.fftGain;
			const filter = audioContext.createBiquadFilter();
			filter.type = "bandpass";
			filter.frequency.value = 15e3;
			filter.Q.value = .05;
			const audioBufferSource = audioContext.createBufferSource();
			audioBufferSource.buffer = audioBuffer;
			audioBufferSource.connect(filter);
			filter.connect(gainNode);
			gainNode.connect(analyser);
			analyser.connect(audioContext.destination);
			audioBufferSource.start(0, startTime, 1 / 30);
			try {
				await audioContext.startRendering();
				signal?.throwIfAborted();
				const frameData = new Uint8Array(this.fftSize / 2);
				analyser.getByteFrequencyData(frameData);
				return frameData;
			} finally {
				audioBufferSource.disconnect();
				analyser.disconnect();
			}
		}));
		const frameLength = framesData[0]?.length ?? 0;
		const smoothedData = new Uint8Array(frameLength);
		for (let i = 0; i < frameLength; i++) {
			let weightedSum = 0;
			let weightSum = 0;
			framesData.forEach((frame, frameIndex) => {
				const decayWeight = DECAY_WEIGHT ** frameIndex;
				weightedSum += (frame[i] ?? 0) * decayWeight;
				weightSum += decayWeight;
			});
			smoothedData[i] = Math.min(255, Math.round(weightedSum / weightSum));
		}
		smoothedData.forEach((value, i) => {
			const freqWeight = this.getFreqWeights()[i] ?? 0;
			smoothedData[i] = Math.min(255, Math.round(value * freqWeight));
		});
		const slicedData = smoothedData.slice(0, Math.floor(smoothedData.length / 2));
		return this.getShouldInterpolateFrequencies() ? processFFTData(slicedData) : slicedData;
	}
	async #analyzeTimeDomain(currentTimeMs, signal) {
		const mediaEngine = await this.getMediaEngine(signal);
		signal?.throwIfAborted();
		if (!mediaEngine?.tracks.audio) return null;
		const frameIntervalMs = 1e3 / 30;
		const earliestFrameMs = currentTimeMs - (this.fftDecay - 1) * frameIntervalMs;
		const fromMs = Math.max(0, earliestFrameMs);
		const maxToMs = currentTimeMs + frameIntervalMs;
		const videoDurationMs = this.intrinsicDurationMs || 0;
		const toMs = videoDurationMs > 0 ? Math.min(maxToMs, videoDurationMs) : maxToMs;
		if (fromMs >= toMs) return null;
		const { fetchAudioSpanningTime: fetchAudioSpan } = await import("./EFMedia/shared/AudioSpanUtils.js");
		let audioSpan;
		try {
			audioSpan = await fetchAudioSpan(this, fromMs, toMs, signal);
		} catch (error) {
			if (error instanceof DOMException && error.name === "AbortError") throw error;
			return null;
		}
		if (!audioSpan?.blob || audioSpan.blob.size < 100) return null;
		const tempAudioContext = new OfflineAudioContext(2, 48e3, 48e3);
		const arrayBuffer = await audioSpan.blob.arrayBuffer();
		signal?.throwIfAborted();
		let audioBuffer;
		try {
			audioBuffer = await tempAudioContext.decodeAudioData(arrayBuffer);
			signal?.throwIfAborted();
		} catch {
			return null;
		}
		const startOffsetMs = audioSpan.startMs;
		const framesData = await Promise.all(Array.from({ length: this.fftDecay }, async (_, i) => {
			const frameOffset = i * (1e3 / 30);
			const startTime = Math.max(0, (currentTimeMs - frameOffset - startOffsetMs) / 1e3);
			const audioContext = new OfflineAudioContext(2, 48e3 / 30, 48e3);
			const analyser = audioContext.createAnalyser();
			analyser.fftSize = this.fftSize;
			analyser.minDecibels = -90;
			analyser.maxDecibels = -10;
			const gainNode = audioContext.createGain();
			gainNode.gain.value = this.fftGain;
			const filter = audioContext.createBiquadFilter();
			filter.type = "bandpass";
			filter.frequency.value = 15e3;
			filter.Q.value = .05;
			const audioBufferSource = audioContext.createBufferSource();
			audioBufferSource.buffer = audioBuffer;
			audioBufferSource.connect(filter);
			filter.connect(gainNode);
			gainNode.connect(analyser);
			analyser.connect(audioContext.destination);
			audioBufferSource.start(0, startTime, 1 / 30);
			try {
				await audioContext.startRendering();
				signal?.throwIfAborted();
				const frameData = new Uint8Array(this.fftSize);
				analyser.getByteTimeDomainData(frameData);
				return frameData;
			} finally {
				audioBufferSource.disconnect();
				analyser.disconnect();
			}
		}));
		const frameLength = framesData[0]?.length ?? 0;
		const smoothedData = new Uint8Array(frameLength);
		for (let i = 0; i < frameLength; i++) {
			let sumSquares = 0;
			framesData.forEach((frame) => {
				const value = (frame[i] ?? 128) - 128;
				sumSquares += value * value;
			});
			const rms = Math.sqrt(sumSquares / framesData.length);
			smoothedData[i] = Math.min(255, Math.max(0, Math.round(rms + 128)));
		}
		return smoothedData;
	}
	/** @deprecated Use fileId instead */
	get assetId() {
		return this.fileId;
	}
	set assetId(value) {
		this.fileId = value;
	}
	get intrinsicDurationMs() {
		return this.#mediaEngine?.durationMs;
	}
	updated(changedProperties) {
		super.updated(changedProperties);
		if (changedProperties.has("src") || changedProperties.has("fileId")) {
			this.getMediaEngine().catch(() => {});
			if (changedProperties.get("src") !== void 0 || changedProperties.get("fileId") !== void 0) this.emitContentChange("source");
		}
		const newCurrentSourceTimeMs = this.currentSourceTimeMs;
		if (newCurrentSourceTimeMs !== this.desiredSeekTimeMs) this.executeSeek(newCurrentSourceTimeMs);
		if (changedProperties.has("ownCurrentTimeMs")) this.executeSeek(this.currentSourceTimeMs);
		if ([
			"_trimStartMs",
			"_trimEndMs",
			"_sourceInMs",
			"_sourceOutMs"
		].some((prop) => changedProperties.has(prop))) {
			this.emitContentChange("bounds");
			if (this.parentTimegroup) {
				this.parentTimegroup.requestUpdate("durationMs");
				this.parentTimegroup.requestUpdate("currentTime");
				let parent = this.parentNode;
				while (parent) {
					if (isContextMixin(parent)) {
						parent.dispatchEvent(new CustomEvent("child-duration-changed", { detail: { source: this } }));
						break;
					}
					parent = parent.parentNode;
				}
			}
		}
	}
	get hasOwnDuration() {
		return true;
	}
	get desiredSeekTimeMs() {
		return this._desiredSeekTimeMs;
	}
	set desiredSeekTimeMs(value) {
		if (this._desiredSeekTimeMs !== value) this._desiredSeekTimeMs = value;
	}
	async executeSeek(seekToMs) {
		this._desiredSeekTimeMs = seekToMs;
	}
	/**
	* Main integration method for EFTimegroup audio playback
	* Now powered by clean, testable utility functions
	* Returns undefined if no audio rendition is available
	*/
	async fetchAudioSpanningTime(fromMs, toMs, signal) {
		return withSpan("media.fetchAudioSpanningTime", {
			elementId: this.id || "unknown",
			tagName: this.tagName.toLowerCase(),
			fromMs,
			toMs,
			durationMs: toMs - fromMs,
			src: this.src || "none"
		}, void 0, async () => {
			const effectiveSignal = signal ?? new AbortController().signal;
			const { fetchAudioSpanningTime } = await import("./EFMedia/shared/AudioSpanUtils.js");
			return fetchAudioSpanningTime(this, fromMs, toMs, effectiveSignal);
		});
	}
	/**
	* Wait for media engine to load and determine duration
	* Ensures media is ready for playback
	*/
	async waitForMediaDurations(signal) {
		if (this.#mediaEngine) return;
		try {
			await this.getMediaEngine(signal);
		} catch (error) {
			const isAbortError = error instanceof DOMException && error.name === "AbortError" || error instanceof Error && (error.name === "AbortError" || error.message.includes("signal is aborted") || error.message.includes("The user aborted a request"));
			if (signal?.aborted) throw error;
			if (isAbortError) return;
			throw error;
		}
	}
	/**
	* Returns media elements for playback audio rendering
	* For standalone media, returns [this]; for timegroups, returns all descendants
	* Used by PlaybackController for audio-driven playback
	*/
	getMediaElements() {
		return [this];
	}
	/**
	* Render audio buffer for playback
	* Called by PlaybackController during live playback
	* Delegates to shared renderTemporalAudio utility for consistent behavior
	*/
	async renderAudio(fromMs, toMs) {
		return renderTemporalAudio(this, fromMs, toMs);
	}
};
__decorate([provide({ context: efContext })], EFMedia.prototype, "efContext", null);
__decorate([property({
	type: Number,
	attribute: "audio-buffer-duration"
})], EFMedia.prototype, "audioBufferDurationMs", void 0);
__decorate([property({
	type: Number,
	attribute: "max-audio-buffer-fetches"
})], EFMedia.prototype, "maxAudioBufferFetches", void 0);
__decorate([property({
	type: Boolean,
	attribute: "enable-audio-buffering"
})], EFMedia.prototype, "enableAudioBuffering", void 0);
__decorate([property({
	type: Boolean,
	attribute: "mute",
	reflect: true
})], EFMedia.prototype, "mute", void 0);
__decorate([property({
	type: Number,
	attribute: "fft-size",
	reflect: true
})], EFMedia.prototype, "fftSize", void 0);
__decorate([property({
	type: Number,
	attribute: "fft-decay",
	reflect: true
})], EFMedia.prototype, "fftDecay", void 0);
__decorate([property({
	type: Number,
	attribute: "fft-gain",
	reflect: true
})], EFMedia.prototype, "fftGain", void 0);
__decorate([property({
	type: Boolean,
	attribute: "interpolate-frequencies",
	reflect: true
})], EFMedia.prototype, "interpolateFrequencies", void 0);
__decorate([property({
	type: String,
	attribute: "file-id",
	reflect: true
})], EFMedia.prototype, "fileId", void 0);
__decorate([state()], EFMedia.prototype, "_desiredSeekTimeMs", void 0);

//#endregion
export { AsyncValue, EFMedia, deepGetMediaElements };
//# sourceMappingURL=EFMedia.js.map