
import {is} from "@e280/stz"
import {Comrade, tune, Thread} from "@e280/comrade"
import {ALL_FORMATS, Input, type StreamTargetChunk} from "mediabunny"

import {Machina} from "./parts/machina.js"
import {Compositor} from "./parts/compositor.js"
import {setupDriverHost} from "./fns/host.js"
import {loadDecoderSource} from "./utils/load-decoder-source.js"
import {DecoderInput, DriverSchematic, Composition, EncoderInput, DecoderSource} from "./fns/schematic.js"

export type DriverOptions = {
	workerUrl: URL | string
}

export class Driver {
	static async setup(options?: DriverOptions) {
		const machina = new Machina()
		const thread = await Comrade.thread<DriverSchematic>({
			label: "OmnitoolDriver",
			workerUrl: options?.workerUrl ?? "/node_modules/@omnimedia/omnitool/x/driver/driver.worker.bundle.min.js",
			setupHost: setupDriverHost(machina),
		})
		const compositor = await Compositor.setup()
		return new this(machina, thread, compositor)
	}

	constructor(
		public machina: Machina,
		public thread: Thread<DriverSchematic>,
		public compositor: Compositor
	) {}

	async hello() {
		return this.thread.work.hello()
	}

	async getAudioDuration(source: DecoderSource) {
		const input = new Input({
			source: await loadDecoderSource(source),
			formats: ALL_FORMATS
		})

		const audioTrack = await input.getPrimaryAudioTrack()
		if (!audioTrack) throw new Error("primary audio track not found")
		return await audioTrack.computeDuration()
	}

	async getVideoDuration(source: DecoderSource) {
		const input = new Input({
			source: await loadDecoderSource(source),
			formats: ALL_FORMATS
		})

		const videoTrack = await input.getPrimaryVideoTrack()
		return await videoTrack?.computeDuration()
	}

	decodeVideo(input: DecoderInput) {
		let lastFrame: VideoFrame | null = null
		const {port1, port2} = new MessageChannel()
		const videoTransform = new TransformStream<VideoFrame, VideoFrame>({
			async transform(chunk, controller) {
				const frame = await input.onFrame?.(chunk) ?? chunk
				lastFrame?.close()
				controller.enqueue(frame)
				lastFrame = frame
			}
		})
		this.thread.work.decodeVideo[tune]({transfer: [videoTransform.writable, port2]})({
			source: input.source,
			cancel: port2,
			video: videoTransform.writable,
			start: input.start,
			end: input.end
		})
		return {
			readable: videoTransform.readable,
			/**
			 * use this to stop decoding (premature interruption)
			 * */
			cancel() {
				port1.postMessage("close")
				port1.close()
			}
		}
	}

	decodeAudio(input: DecoderInput) {
		const audioTransform = new TransformStream<AudioData, AudioData>()
		const {port1, port2} = new MessageChannel()
		this.thread.work.decodeAudio[tune]({transfer: [audioTransform.writable, port2]})({
			source: input.source,
			cancel: port2,
			audio: audioTransform.writable,
			start: input.start,
			end: input.end
		})
		return {
			readable: audioTransform.readable,
			/**
			 * use this to stop decoding (premature interruption)
			 * */
			cancel() {
				port1.postMessage("close")
				port1.close()
			}
		}
	}

	encode({audio, video, config}: EncoderInput) {
		const {readable, writable} = new TransformStream<StreamTargetChunk, StreamTargetChunk>()
		const transfer = [audio, video, writable].filter(is.happy)
		const done = this.thread.work.encode[tune]({transfer})({audio, video, config, writable})
		return {readable, done}
	}

	async composite(
		composition: Composition,
	) {
		return await this.compositor.composite(composition)
	}

	resize(width: number, height: number) {
		this.compositor.resize(width, height)
	}

}

