import type { MouseEvent as ReactMouseEvent } from "react";
import { snappedSeekTime, type FrameRate } from "opencut-wasm";
import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/timeline/scale";
import { TICKS_PER_SECOND } from "@/wasm";

type SeekSource = "ruler" | "tracks";

interface PendingSeekSession {
	kind: "pending";
	source: SeekSource;
	downX: number;
	downY: number;
	downTime: number;
}

type Session = { kind: "idle" } | PendingSeekSession;

export interface SeekConfig {
	zoomLevel: number;
	duration: number;
	isSelecting: boolean;
	getPlayheadEl: () => HTMLDivElement | null;
	getTrackLabelsEl: () => HTMLDivElement | null;
	getRulerScrollEl: () => HTMLDivElement | null;
	getTracksScrollEl: () => HTMLDivElement | null;
	getActiveProjectFps: () => FrameRate | null;
	clearSelectedElements: () => void;
	seek: (time: number) => void;
	setTimelineViewState: (viewState: {
		zoomLevel: number;
		scrollLeft: number;
		playheadTime: number;
	}) => void;
}

export interface SeekConfigRef {
	readonly current: SeekConfig;
}

function pixelToTime({
	clientX,
	scrollContainer,
	zoomLevel,
	duration,
}: {
	clientX: number;
	scrollContainer: HTMLDivElement;
	zoomLevel: number;
	duration: number;
}): number {
	const rect = scrollContainer.getBoundingClientRect();
	const mouseX = clientX - rect.left;
	const scrollLeft = scrollContainer.scrollLeft;

	const rawTimeSeconds = Math.max(
		0,
		Math.min(
			duration / TICKS_PER_SECOND,
			(mouseX + scrollLeft) / (BASE_TIMELINE_PIXELS_PER_SECOND * zoomLevel),
		),
	);

	return Math.round(rawTimeSeconds * TICKS_PER_SECOND);
}

function isClickGesture({
	event,
	session,
}: {
	event: ReactMouseEvent;
	session: PendingSeekSession;
}): boolean {
	const deltaX = Math.abs(event.clientX - session.downX);
	const deltaY = Math.abs(event.clientY - session.downY);
	const deltaTime = event.timeStamp - session.downTime;

	return deltaX <= 5 && deltaY <= 5 && deltaTime <= 500;
}

export class SeekController {
	private session: Session = { kind: "idle" };
	private readonly configRef: SeekConfigRef;

	constructor(deps: { configRef: SeekConfigRef }) {
		this.configRef = deps.configRef;
		this.onTracksMouseDown = this.onTracksMouseDown.bind(this);
		this.onRulerMouseDown = this.onRulerMouseDown.bind(this);
		this.onTracksClick = this.onTracksClick.bind(this);
		this.onRulerClick = this.onRulerClick.bind(this);
	}

	private get config(): SeekConfig {
		return this.configRef.current;
	}

	destroy(): void {
		this.session = { kind: "idle" };
	}

	onTracksMouseDown(event: ReactMouseEvent): void {
		this.beginPendingSeek({ event, source: "tracks" });
	}

	onRulerMouseDown(event: ReactMouseEvent): void {
		this.beginPendingSeek({ event, source: "ruler" });
	}

	onTracksClick(event: ReactMouseEvent): void {
		this.handleClick({ event, source: "tracks" });
	}

	onRulerClick(event: ReactMouseEvent): void {
		this.handleClick({ event, source: "ruler" });
	}

	private beginPendingSeek({
		event,
		source,
	}: {
		event: ReactMouseEvent;
		source: SeekSource;
	}): void {
		if (event.button !== 0) return;

		this.session = {
			kind: "pending",
			source,
			downX: event.clientX,
			downY: event.clientY,
			downTime: event.timeStamp,
		};
	}

	private handleClick({
		event,
		source,
	}: {
		event: ReactMouseEvent;
		source: SeekSource;
	}): void {
		const shouldProcess = this.shouldProcessClick({ event, source });
		this.session = { kind: "idle" };

		if (!shouldProcess) return;

		this.config.clearSelectedElements();
		this.seekFromEvent({ event, source });
	}

	private shouldProcessClick({
		event,
		source,
	}: {
		event: ReactMouseEvent;
		source: SeekSource;
	}): boolean {
		if (this.session.kind !== "pending") return false;
		if (this.session.source !== source) return false;
		if (!isClickGesture({ event, session: this.session })) return false;
		if (this.config.isSelecting) return false;

		const target = event.target as HTMLElement;
		if (this.config.getPlayheadEl()?.contains(target)) return false;

		if (this.config.getTrackLabelsEl()?.contains(target)) {
			this.config.clearSelectedElements();
			return false;
		}

		return true;
	}

	private seekFromEvent({
		event,
		source,
	}: {
		event: ReactMouseEvent;
		source: SeekSource;
	}): void {
		const scrollContainer =
			source === "ruler"
				? this.config.getRulerScrollEl()
				: this.config.getTracksScrollEl();
		if (!scrollContainer) return;

		const rawTime = pixelToTime({
			clientX: event.clientX,
			scrollContainer,
			zoomLevel: this.config.zoomLevel,
			duration: this.config.duration,
		});

		const fps = this.config.getActiveProjectFps();
		const time =
			fps != null
				? (snappedSeekTime({
						time: rawTime,
						duration: this.config.duration,
						rate: fps,
					}) ?? rawTime)
				: rawTime;

		this.config.seek(time);
		this.config.setTimelineViewState({
			zoomLevel: this.config.zoomLevel,
			scrollLeft: scrollContainer.scrollLeft,
			playheadTime: time,
		});
	}
}
