import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
import { isContextMixin } from "../gui/ContextMixin.js";
import { durationConverter } from "./durationConverter.js";
import { EFTemporal, deepGetElementsWithFrameTasks, flushStartTimeMsCache, shallowGetTemporalElements, timegroupContext } from "./EFTemporal.js";
import { updateAnimations } from "./updateAnimations.js";
import { deepGetMediaElements } from "./EFMedia.js";
import { TimegroupController } from "./TimegroupController.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");
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: EF_INTERACTIVE,
			args: () => [this.ownCurrentTimeMs, this.currentTimeMs],
			task: async ([], { signal }) => {
				if (this.isRootTimegroup) await this.waitForFrameTasks(signal);
			}
		});
		this.seekTask = new Task(this, {
			args: () => [this.#pendingSeekTime ?? this.#currentTime],
			task: async ([targetTime], { signal }) => {
				const newTime = Math.max(0, Math.min(targetTime, this.durationMs / 1e3));
				this.#currentTime = newTime;
				this.requestUpdate("currentTime");
				await this.updateComplete;
				signal.throwIfAborted();
				const videoElements = this.querySelectorAll("ef-video");
				for (const video of videoElements) if (video.videoSeekTask) video.videoSeekTask.run();
				await this.frameTask.run();
				this.#saveTimeToLocalStorage(newTime);
			}
		});
	}
	static {
		this.styles = css`
    :host {
      display: block;
      width: 100%;
      height: 100%;
      position: absolute;
      top: 0;
      left: 0;
    }
  `;
	}
	#currentTime = 0;
	#resizeObserver;
	#seekInProgress = false;
	#pendingSeekTime;
	set currentTime(time) {
		if (this.#seekInProgress) {
			this.#pendingSeekTime = time;
			return;
		}
		this.#seekInProgress = true;
		this.#pendingSeekTime = time;
		this.seekTask.run().finally(() => {
			this.#seekInProgress = false;
			if (this.#pendingSeekTime !== void 0 && this.#pendingSeekTime !== time) {
				const pendingTime = this.#pendingSeekTime;
				this.#pendingSeekTime = void 0;
				this.currentTime = pendingTime;
			} else this.#pendingSeekTime = void 0;
		});
	}
	get currentTime() {
		return this.#currentTime;
	}
	get currentTimeMs() {
		return this.currentTime * 1e3;
	}
	set currentTimeMs(ms) {
		this.currentTime = ms / 1e3;
	}
	/**
	* Determines if this is a root timegroup (no parent timegroups)
	*/
	get isRootTimegroup() {
		return this.closest("ef-timegroup") === this;
	}
	/**
	* Saves time to localStorage (extracted for reuse)
	*/
	#saveTimeToLocalStorage(time) {
		try {
			if (this.id && this.isConnected) localStorage.setItem(this.storageKey, time.toString());
		} catch (error) {
			log("Failed to save time to localStorage", error);
		}
	}
	render() {
		return html`<slot></slot> `;
	}
	maybeLoadTimeFromLocalStorage() {
		if (this.id) try {
			return Number.parseFloat(localStorage.getItem(this.storageKey) || "0");
		} catch (error) {
			log("Failed to load time from localStorage", error);
		}
		return 0;
	}
	connectedCallback() {
		super.connectedCallback();
		if (this.id) this.waitForMediaDurations().then(() => {
			this.currentTime = this.maybeLoadTimeFromLocalStorage();
		});
		if (this.parentTimegroup) new TimegroupController(this.parentTimegroup, this);
		if (this.shouldWrapWithWorkbench()) this.wrapWithWorkbench();
		requestAnimationFrame(() => {
			this.updateAnimations();
		});
	}
	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": {
				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;
				});
				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);
		return temporals.map((temporal) => temporal.frameTask).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(signal) {
		const limit = 10;
		let step = 0;
		await this.waitForNestedUpdates(signal);
		while (step < limit) {
			step++;
			let pendingTasks = await this.getPendingFrameTasks(signal);
			signal?.throwIfAborted();
			await Promise.all(pendingTasks.map((task) => task.taskComplete));
			signal?.throwIfAborted();
			await this.updateComplete;
			signal?.throwIfAborted();
			pendingTasks = await this.getPendingFrameTasks(signal);
			if (pendingTasks.length === 0) break;
		}
	}
	/**
	* 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.taskComplete));
		flushStartTimeMsCache();
		this.requestUpdate("currentTime");
		await this.updateComplete;
	}
	get childTemporals() {
		return shallowGetTemporalElements(this);
	}
	updated(changedProperties) {
		super.updated(changedProperties);
		if (changedProperties.has("currentTime") || changedProperties.has("ownCurrentTimeMs")) this.updateAnimations();
	}
	updateAnimations() {
		updateAnimations(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 audio = await mediaElement.fetchAudioSpanningTime(mediaLocalFromMs, mediaLocalToMs, 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 ctxEndMs = mediaElement.endTimeMs - fromMs;
			const ctxDurationMs = ctxEndMs - ctxStartMs;
			const requestedOffsetInMedia = mediaLocalFromMs;
			const actualOffsetInBuffer = requestedOffsetInMedia - audio.startMs;
			const safeOffset = Math.max(0, actualOffsetInBuffer);
			if (safeOffset !== actualOffsetInBuffer) {}
			bufferSource.start(ctxStartMs / 1e3, safeOffset / 1e3, ctxDurationMs / 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", void 0);
_decorate([property({
	type: Number,
	converter: durationConverter,
	attribute: "overlap"
})], EFTimegroup.prototype, "overlapMs", void 0);
_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 };
