"use client"

import type { IVideoFramesTrackProps } from "./VideoFramesTrack.d"
import type { JSX } from "react"

import {
	FunctionComponent,
	useCallback,
	useEffect,
	useRef,
	useState,
} from "react"

import { useMediaRemote } from "@vidstack/react"

import api from "@shared/api"

import styles from "./VideoFramesTrack.module.scss"

const MAX_CACHED_IMAGES = 800
const VIRTUAL_OVERSCAN_PX = 600
const DEBOUNCE_MS = 300

function getVirtualCanvasWindow(
	totalWidth: number,
	scrollLeft: number,
	viewportWidth: number,
): {
	startX: number
	endX: number
	width: number
} {
	const safeViewportWidth = Math.max(1, viewportWidth)
	const overscan = Math.max(VIRTUAL_OVERSCAN_PX, safeViewportWidth * 0.5)
	const startX = Math.max(0, Math.floor(scrollLeft - overscan))
	const endX = Math.min(
		totalWidth,
		Math.ceil(scrollLeft + safeViewportWidth + overscan),
	)

	return {
		startX,
		endX,
		width: Math.max(1, endX - startX),
	}
}

function useDebouncedValue<T>(value: T, delayMs: number): T {
	const [debounced, setDebounced] = useState(value)
	useEffect(() => {
		const timer = setTimeout(() => setDebounced(value), delayMs)
		return () => clearTimeout(timer)
	}, [value, delayMs])
	return debounced
}

export const VideoFramesTrack: FunctionComponent<
	IVideoFramesTrackProps
> = ({
	videoUrl,
	fileKey,
	pixelsPerSecond,
	height,
	duration,
	scrollLeft = 0,
	viewportWidth = 2000,
}): JSX.Element => {
	const remote = useMediaRemote()

	const canvasRef = useRef<HTMLCanvasElement>(null)
	const imageCacheRef = useRef<Map<number, HTMLImageElement>>(new Map())
	const cacheOrderRef = useRef<number[]>([])
	const pendingTimesRef = useRef<Set<number>>(new Set())
	const drawRafRef = useRef(0)

	const [drawVersion, setDrawVersion] = useState(0)

	const totalWidth = Math.max(duration * pixelsPerSecond, 200)
	const frameWidth = Math.round((height * 16) / 9)
	const virtualWindow = getVirtualCanvasWindow(
		totalWidth,
		scrollLeft,
		viewportWidth,
	)
	const canvasOffsetX = virtualWindow.startX

	// Compute the visible time range based on viewport
	const visibleStart = Math.max(0, scrollLeft / pixelsPerSecond)
	const visibleEnd = Math.min(
		duration,
		(scrollLeft + viewportWidth) / pixelsPerSecond,
	)
	const overscanSeconds = VIRTUAL_OVERSCAN_PX / pixelsPerSecond
	const rangeStart = Math.max(0, visibleStart - overscanSeconds)
	const rangeEnd = Math.min(duration, visibleEnd + overscanSeconds)

	// Debounce range to avoid refetching on every scroll pixel
	const debouncedStart = useDebouncedValue(Math.floor(rangeStart), DEBOUNCE_MS)
	const debouncedEnd = useDebouncedValue(
		Math.ceil(rangeEnd) + 1,
		DEBOUNCE_MS,
	)

	const queryEnabled =
		!!fileKey && duration > 0 && pixelsPerSecond > 0 && debouncedEnd > 0

	const { data: framesData } = api.useQuery(
		"get",
		"/api/media/frames/",
		{
			params: {
				query: {
					file_key: fileKey,
					start: debouncedStart,
					end: debouncedEnd,
				},
			},
		},
		{
			enabled: queryEnabled,
			staleTime: 60_000,
			refetchOnWindowFocus: false,
		},
	)

	const scheduleDraw = useCallback(() => {
		if (drawRafRef.current) return
		drawRafRef.current = requestAnimationFrame(() => {
			drawRafRef.current = 0
			setDrawVersion((prev) => prev + 1)
		})
	}, [])

	const cacheImage = useCallback((timestamp: number, img: HTMLImageElement) => {
		const cache = imageCacheRef.current
		const order = cacheOrderRef.current

		if (!cache.has(timestamp)) {
			order.push(timestamp)
		}
		cache.set(timestamp, img)

		while (order.length > MAX_CACHED_IMAGES) {
			const oldestTime = order.shift()
			if (typeof oldestTime === "number") {
				cache.delete(oldestTime)
			}
		}
	}, [])

	// Load frame images when query data changes
	useEffect(() => {
		if (!framesData?.frames) return

		for (const frame of framesData.frames) {
			if (
				imageCacheRef.current.has(frame.timestamp) ||
				pendingTimesRef.current.has(frame.timestamp)
			) {
				continue
			}

			pendingTimesRef.current.add(frame.timestamp)
			const img = new Image()
			img.onload = () => {
				pendingTimesRef.current.delete(frame.timestamp)
				cacheImage(frame.timestamp, img)
				scheduleDraw()
			}
			img.onerror = () => {
				pendingTimesRef.current.delete(frame.timestamp)
			}
			img.src = frame.url
		}
	}, [framesData, cacheImage, scheduleDraw])

	// Clear cache when fileKey changes
	useEffect(() => {
		return () => {
			imageCacheRef.current.clear()
			cacheOrderRef.current = []
			pendingTimesRef.current.clear()
		}
	}, [fileKey])

	// Cleanup on unmount
	useEffect(() => {
		return () => {
			if (drawRafRef.current) cancelAnimationFrame(drawRafRef.current)
			imageCacheRef.current.clear()
			cacheOrderRef.current = []
		}
	}, [])

	// Draw frames onto canvas
	useEffect(() => {
		const canvas = canvasRef.current
		if (!canvas) return

		const ctx = canvas.getContext("2d")
		if (!ctx) return

		const dpr = window.devicePixelRatio || 1
		const targetWidth = Math.max(1, Math.round(virtualWindow.width * dpr))
		const targetHeight = Math.max(1, Math.round(height * dpr))
		const needsResize =
			canvas.width !== targetWidth || canvas.height !== targetHeight

		if (needsResize) {
			canvas.width = targetWidth
			canvas.height = targetHeight
		}
		canvas.style.width = `${virtualWindow.width}px`
		canvas.style.height = `${height}px`
		canvas.style.left = `${virtualWindow.startX}px`

		ctx.setTransform(dpr, 0, 0, dpr, 0, 0)

		const visibleStartPx = virtualWindow.startX
		const visibleEndPx = virtualWindow.endX

		ctx.fillStyle = "#1a1a2e"
		ctx.clearRect(0, 0, virtualWindow.width, height)
		ctx.fillRect(0, 0, virtualWindow.width, height)

		imageCacheRef.current.forEach((img, time) => {
			const x = time * pixelsPerSecond
			if (x + frameWidth < visibleStartPx || x > visibleEndPx) return
			ctx.drawImage(img, x - virtualWindow.startX, 0, frameWidth, height)
		})
	}, [
		drawVersion,
		frameWidth,
		height,
		pixelsPerSecond,
		totalWidth,
		virtualWindow.endX,
		virtualWindow.startX,
		virtualWindow.width,
	])

	const handleClick = useCallback(
		(e: React.MouseEvent<HTMLCanvasElement>) => {
			const rect = e.currentTarget.getBoundingClientRect()
			const x = e.clientX - rect.left
			const globalX = canvasOffsetX + x
			const time = globalX / pixelsPerSecond
			if (time >= 0 && time <= duration) remote.seek(time)
		},
		[canvasOffsetX, duration, pixelsPerSecond, remote],
	)

	return (
		<div
			className={styles.root}
			style={{ width: totalWidth, height }}
		>
			<canvas
				ref={canvasRef}
				className={styles.canvas}
				data-testid="VideoFramesTrack"
				onClick={handleClick}
			/>
		</div>
	)
}
