import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
import { isContextMixin } from "../gui/ContextMixin.js";
import { durationConverter } from "./durationConverter.js";
import { EFTemporal, deepGetElementsWithFrameTasks, flushStartTimeMsCache, resetTemporalCache, shallowGetTemporalElements, timegroupContext } from "./EFTemporal.js";
import { deepGetMediaElements } from "./EFMedia.js";
import { TimegroupController } from "./TimegroupController.js";
import { evaluateTemporalState, updateAnimations } from "./updateAnimations.js";
import { provide } from "@lit/context";
import { Task, TaskStatus } from "@lit/task";
import debug from "debug";
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import _decorate from "@oxc-project/runtime/helpers/decorate";
var _EFTimegroup;
const log = debug("ef:elements:EFTimegroup");
let sequenceDurationCache = /* @__PURE__ */ new WeakMap();
const flushSequenceDurationCache = () => {
	sequenceDurationCache = /* @__PURE__ */ new WeakMap();
};
const shallowGetTimegroups = (element, groups = []) => {
	for (const child of Array.from(element.children)) if (child instanceof EFTimegroup) groups.push(child);
	else shallowGetTimegroups(child, groups);
	return groups;
};
let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
	static {
		_EFTimegroup = this;
	}
	constructor(..._args) {
		super(..._args);
		this._timeGroupContext = this;
		this._mode = "contain";
		this._overlapMs = 0;
		this.fit = "none";
		this.frameTask = new Task(this, {
			autoRun: false,
			args: () => [this.ownCurrentTimeMs, this.currentTimeMs],
			task: async ([]) => {
				if (this.isRootTimegroup) {
					await this.waitForFrameTasks();
					updateAnimations(this);
				}
			}
		});
		this.seekTask = new Task(this, {
			autoRun: false,
			args: () => [this.#pendingSeekTime ?? this.#currentTime],
			onComplete: () => {},
			task: async ([targetTime]) => {
				if (!this.isRootTimegroup) return;
				await this.waitForMediaDurations();
				const newTime = Math.max(0, Math.min(targetTime ?? 0, this.durationMs / 1e3));
				this.requestUpdate("currentTime");
				await this.frameTask.run();
				this.#saveTimeToLocalStorage(newTime);
				this.#seekInProgress = false;
				return newTime;
			}
		});
	}
	static {
		this.styles = css`
    :host {
      display: block;
      width: 100%;
      height: 100%;
      position: absolute;
      top: 0;
      left: 0;
    }
  `;
	}
	#currentTime = void 0;
	set mode(value) {
		sequenceDurationCache.delete(this);
		this._mode = value;
	}
	get mode() {
		return this._mode;
	}
	set overlapMs(value) {
		sequenceDurationCache.delete(this);
		this._overlapMs = value;
	}
	get overlapMs() {
		return this._overlapMs;
	}
	#resizeObserver;
	#seekInProgress = false;
	#pendingSeekTime;
	#processingPendingSeek = false;
	set currentTime(time) {
		if (!this.isRootTimegroup) return;
		if (Number.isNaN(time)) return;
		if (time === this.#currentTime && !this.#processingPendingSeek) return;
		if (this.#pendingSeekTime === time) return;
		if (this.#seekInProgress) {
			console.trace("pending seek to", time);
			this.#pendingSeekTime = time;
			this.#currentTime = time;
			return;
		}
		console.trace("seeking to", time);
		this.#currentTime = time;
		this.#seekInProgress = true;
		this.seekTask.run().finally(() => {
			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;
		});
	}
	get currentTime() {
		return this.#currentTime ?? 0;
	}
	set currentTimeMs(ms) {
		this.currentTime = ms / 1e3;
	}
	get currentTimeMs() {
		return this.currentTime * 1e3;
	}
	/**
	* Determines if this is a root timegroup (no parent timegroups)
	*/
	get isRootTimegroup() {
		return !this.parentTimegroup;
	}
	/**
	* Saves time to localStorage (extracted for reuse)
	*/
	#saveTimeToLocalStorage(time) {
		try {
			if (this.id && this.isConnected && !Number.isNaN(time)) localStorage.setItem(this.storageKey, time.toString());
		} catch (error) {
			log("Failed to save time to localStorage", error);
		}
	}
	render() {
		return html`<slot @slotchange=${this.#handleSlotChange}></slot> `;
	}
	#handleSlotChange = () => {
		resetTemporalCache();
		flushSequenceDurationCache();
		flushStartTimeMsCache();
		this.requestUpdate();
	};
	maybeLoadTimeFromLocalStorage() {
		if (this.id) try {
			const storedValue = localStorage.getItem(this.storageKey);
			if (storedValue === null) return void 0;
			return Number.parseFloat(storedValue);
		} catch (error) {
			log("Failed to load time from localStorage", error);
		}
	}
	connectedCallback() {
		super.connectedCallback();
		if (this.id) this.waitForMediaDurations().then(() => {
			const maybeLoadedTime = this.maybeLoadTimeFromLocalStorage();
			if (maybeLoadedTime !== void 0) this.currentTime = maybeLoadedTime;
		});
		if (this.parentTimegroup) new TimegroupController(this.parentTimegroup, this);
		if (this.shouldWrapWithWorkbench()) this.wrapWithWorkbench();
	}
	disconnectedCallback() {
		super.disconnectedCallback();
		this.#resizeObserver?.disconnect();
	}
	get storageKey() {
		if (!this.id) throw new Error("Timegroup must have an id to use localStorage.");
		return `ef-timegroup-${this.id}`;
	}
	get intrinsicDurationMs() {
		if (this.hasExplicitDuration) return this.explicitDurationMs;
		return void 0;
	}
	get hasOwnDuration() {
		return this.mode === "contain" || this.mode === "sequence" || this.mode === "fixed" && this.hasExplicitDuration;
	}
	get durationMs() {
		switch (this.mode) {
			case "fit": {
				if (!this.parentTimegroup) return 0;
				return this.parentTimegroup.durationMs;
			}
			case "fixed": return super.durationMs;
			case "sequence": {
				const cachedDuration = sequenceDurationCache.get(this);
				if (cachedDuration !== void 0) return cachedDuration;
				let duration = 0;
				this.childTemporals.forEach((child, index) => {
					if (child instanceof _EFTimegroup && child.mode === "fit") return;
					if (index > 0) duration -= this.overlapMs;
					duration += child.durationMs;
				});
				sequenceDurationCache.set(this, duration);
				return duration;
			}
			case "contain": {
				let maxDuration = 0;
				for (const child of this.childTemporals) {
					if (child instanceof _EFTimegroup && child.mode === "fit") continue;
					if (!child.hasOwnDuration) continue;
					maxDuration = Math.max(maxDuration, child.durationMs);
				}
				return maxDuration;
			}
			default: throw new Error(`Invalid time mode: ${this.mode}`);
		}
	}
	async getPendingFrameTasks(signal) {
		await this.waitForNestedUpdates(signal);
		signal?.throwIfAborted();
		const temporals = deepGetElementsWithFrameTasks(this);
		const timelineTimeMs = (this.#pendingSeekTime ?? this.#currentTime ?? 0) * 1e3;
		const activeTemporals = temporals.filter((temporal) => {
			if (!("startTimeMs" in temporal) || !("endTimeMs" in temporal)) return true;
			const epsilon = .001;
			const startTimeMs = temporal.startTimeMs;
			const endTimeMs = temporal.endTimeMs;
			const elementStartsBeforeEnd = startTimeMs <= timelineTimeMs + epsilon;
			const elementEndsAfterStart = endTimeMs > timelineTimeMs;
			return elementStartsBeforeEnd && elementEndsAfterStart;
		});
		const frameTasks = activeTemporals.map((temporal) => temporal.frameTask);
		frameTasks.forEach((task) => {
			task.run();
		});
		return frameTasks.filter((task) => task.status < TaskStatus.COMPLETE);
	}
	async waitForNestedUpdates(signal) {
		const limit = 10;
		let steps = 0;
		let isComplete = true;
		while (true) {
			steps++;
			if (steps > limit) throw new Error("Reached update depth limit.");
			isComplete = await this.updateComplete;
			signal?.throwIfAborted();
			if (isComplete) break;
		}
	}
	async waitForFrameTasks() {
		const temporalElements = deepGetElementsWithFrameTasks(this);
		const visibleElements = temporalElements.filter((element) => {
			const temporalState = evaluateTemporalState(element);
			return temporalState.isVisible;
		});
		await Promise.all(visibleElements.map((element) => {
			return element.frameTask.run();
		}));
	}
	/**
	* Wait for all media elements to load their initial segments.
	* Ideally we would only need the extracted index json data, but
	* that caused issues with constructing audio data. We had negative durations
	* in calculations and it was not clear why.
	*/
	async waitForMediaDurations() {
		await this.updateComplete;
		const mediaElements = deepGetMediaElements(this);
		await Promise.all(mediaElements.map((m) => m.mediaEngineTask.value ? Promise.resolve() : m.mediaEngineTask.run()));
		flushStartTimeMsCache();
		flushSequenceDurationCache();
		this.requestUpdate("currentTime");
		await this.updateComplete;
	}
	get childTemporals() {
		return shallowGetTemporalElements(this);
	}
	get contextProvider() {
		let parent = this.parentNode;
		while (parent) {
			if (isContextMixin(parent)) return parent;
			parent = parent.parentNode;
		}
		return null;
	}
	/**
	* Returns true if the timegroup should be wrapped with a workbench.
	*
	* A timegroup should be wrapped with a workbench if it is the root-most timegroup
	* and EF_INTERACTIVE is true.
	*
	* If the timegroup is already wrappedin a context provider like ef-preview,
	* it should NOT be wrapped in a workbench.
	*/
	shouldWrapWithWorkbench() {
		return EF_INTERACTIVE && this.closest("ef-timegroup") === this && this.closest("ef-preview") === null && this.closest("ef-workbench") === null && this.closest("test-context") === null;
	}
	wrapWithWorkbench() {
		const workbench = document.createElement("ef-workbench");
		this.parentElement?.append(workbench);
		if (!this.hasAttribute("id")) this.setAttribute("id", "root-this");
		this.setAttribute("slot", "canvas");
		workbench.append(this);
		const filmstrip = document.createElement("ef-filmstrip");
		filmstrip.setAttribute("slot", "timeline");
		filmstrip.setAttribute("target", this.id);
		workbench.append(filmstrip);
	}
	get efElements() {
		return Array.from(this.querySelectorAll("ef-audio, ef-video, ef-image, ef-captions, ef-waveform"));
	}
	async #addAudioToContext(audioContext, fromMs, toMs) {
		await this.waitForMediaDurations();
		const abortController = new AbortController();
		await Promise.all(deepGetMediaElements(this).map(async (mediaElement) => {
			if (mediaElement.mute) return;
			const mediaStartsBeforeEnd = mediaElement.startTimeMs <= toMs;
			const mediaEndsAfterStart = mediaElement.endTimeMs >= fromMs;
			const mediaOverlaps = mediaStartsBeforeEnd && mediaEndsAfterStart;
			if (!mediaOverlaps) return;
			const mediaLocalFromMs = Math.max(0, fromMs - mediaElement.startTimeMs);
			const mediaLocalToMs = Math.min(mediaElement.endTimeMs - mediaElement.startTimeMs, toMs - mediaElement.startTimeMs);
			if (mediaLocalFromMs >= mediaLocalToMs) return;
			const sourceInMs = mediaElement.sourceInMs || mediaElement.trimStartMs || 0;
			const mediaSourceFromMs = mediaLocalFromMs + sourceInMs;
			const mediaSourceToMs = mediaLocalToMs + sourceInMs;
			const audio = await mediaElement.fetchAudioSpanningTime(mediaSourceFromMs, mediaSourceToMs, abortController.signal);
			if (!audio) throw new Error("Failed to fetch audio");
			const bufferSource = audioContext.createBufferSource();
			bufferSource.buffer = await audioContext.decodeAudioData(await audio.blob.arrayBuffer());
			bufferSource.connect(audioContext.destination);
			const ctxStartMs = Math.max(0, mediaElement.startTimeMs - fromMs);
			const requestedSourceFromMs = mediaSourceFromMs;
			const actualSourceStartMs = audio.startMs;
			const offsetInBufferMs = requestedSourceFromMs - actualSourceStartMs;
			const safeOffsetMs = Math.max(0, offsetInBufferMs);
			const requestedDurationMs = mediaSourceToMs - mediaSourceFromMs;
			const availableAudioMs = audio.endMs - audio.startMs;
			const actualDurationMs = Math.min(requestedDurationMs, availableAudioMs - safeOffsetMs);
			if (actualDurationMs <= 0) return;
			bufferSource.start(ctxStartMs / 1e3, safeOffsetMs / 1e3, actualDurationMs / 1e3);
		}));
	}
	async renderAudio(fromMs, toMs) {
		const durationMs = toMs - fromMs;
		const duration = durationMs / 1e3;
		const exactSamples = 48e3 * duration;
		const aacFrames = exactSamples / 1024;
		const alignedFrames = Math.round(aacFrames);
		const contextSize = alignedFrames * 1024;
		if (contextSize <= 0) throw new Error(`Duration must be greater than 0 when rendering audio. ${contextSize}ms`);
		let audioContext;
		try {
			audioContext = new OfflineAudioContext(2, contextSize, 48e3);
		} catch (error) {
			throw new Error(`[EFTimegroup.renderAudio] Failed to create OfflineAudioContext(2, ${contextSize}, 48000) for renderAudio(${fromMs}, ${toMs}) with contextSize=${contextSize}: ${error instanceof Error ? error.message : String(error)}. This typically happens when audio parameters are invalid (e.g., contextSize <= 0).`);
		}
		await this.#addAudioToContext(audioContext, fromMs, toMs);
		const renderedBuffer = await audioContext.startRendering();
		return renderedBuffer;
	}
	/**
	* TEMPORARY TEST METHOD: Renders audio and immediately plays it back
	* Usage: timegroup.testPlayAudio(0, 5000) // Play first 5 seconds
	*/
	async testPlayAudio(fromMs, toMs) {
		const renderedBuffer = await this.renderAudio(fromMs, toMs);
		const playbackContext = new AudioContext();
		const bufferSource = playbackContext.createBufferSource();
		bufferSource.buffer = renderedBuffer;
		bufferSource.connect(playbackContext.destination);
		bufferSource.start(0);
		return new Promise((resolve) => {
			bufferSource.onended = () => {
				playbackContext.close();
				resolve();
			};
		});
	}
	async loadMd5Sums() {
		const efElements = this.efElements;
		const loaderTasks = [];
		for (const el of efElements) {
			const md5SumLoader = el.md5SumLoader;
			if (md5SumLoader instanceof Task) {
				md5SumLoader.run();
				loaderTasks.push(md5SumLoader.taskComplete);
			}
		}
		await Promise.all(loaderTasks);
		efElements.map((el) => {
			if ("productionSrc" in el && el.productionSrc instanceof Function) el.setAttribute("src", el.productionSrc());
		});
	}
};
_decorate([provide({ context: timegroupContext })], EFTimegroup.prototype, "_timeGroupContext", void 0);
_decorate([property({
	type: String,
	attribute: "mode"
})], EFTimegroup.prototype, "mode", null);
_decorate([property({
	type: Number,
	converter: durationConverter,
	attribute: "overlap"
})], EFTimegroup.prototype, "overlapMs", null);
_decorate([property({ type: String })], EFTimegroup.prototype, "fit", void 0);
_decorate([property({
	type: Number,
	attribute: "currenttime"
})], EFTimegroup.prototype, "currentTime", null);
EFTimegroup = _EFTimegroup = _decorate([customElement("ef-timegroup")], EFTimegroup);
export { EFTimegroup, shallowGetTimegroups };
