import type { MouseEvent as ReactMouseEvent } from "react";
import { roundToFrame, snappedSeekTime, type FrameRate } from "opencut-wasm";
import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/timeline/scale";
import { TICKS_PER_SECOND } from "@/wasm";
import { TIMELINE_DRAG_THRESHOLD_PX } from "@/timeline/components/interaction";
import { timelineTimeToSnappedPixels } from "@/timeline";
import { getKeyframeById } from "@/animation";
import { RetimeKeyframeCommand } from "@/commands/timeline/element/keyframes/retime-keyframe";
import { BatchCommand } from "@/commands";
import type { SelectedKeyframeRef } from "@/animation/types";
import type { TimelineElement } from "@/timeline";
import type { Command } from "@/commands/base-command";

// --- Session ---

interface PendingSession {
	kind: "pending";
	keyframeRefs: SelectedKeyframeRef[];
	startMouseX: number;
}

interface ActiveSession {
	kind: "active";
	keyframeRefs: SelectedKeyframeRef[];
	startMouseX: number;
	deltaTicks: number;
}

type Session = { kind: "idle" } | PendingSession | ActiveSession;

// --- Public state ---

export interface KeyframeDragState {
	isDragging: boolean;
	draggingKeyframeIds: Set<string>;
	deltaTicks: number;
}

const IDLE_DRAG_STATE: KeyframeDragState = {
	isDragging: false,
	draggingKeyframeIds: new Set(),
	deltaTicks: 0,
};

// --- Config ---

export interface KeyframeDragConfig {
	zoomLevel: number;
	getFps: () => FrameRate | null;
	element: TimelineElement;
	displayedStartTime: number;
	selectedKeyframes: SelectedKeyframeRef[];
	isKeyframeSelected: (args: { keyframe: SelectedKeyframeRef }) => boolean;
	setKeyframeSelection: (args: { keyframes: SelectedKeyframeRef[] }) => void;
	toggleKeyframeSelection: (args: {
		keyframes: SelectedKeyframeRef[];
		isMultiKey: boolean;
	}) => void;
	selectKeyframeRange: (args: {
		orderedKeyframes: SelectedKeyframeRef[];
		targetKeyframes: SelectedKeyframeRef[];
		isAdditive: boolean;
	}) => void;
	executeCommand: (command: Command) => void;
	seek: (args: { time: number }) => void;
	getTotalDuration: () => number;
}

export interface KeyframeDragConfigRef {
	readonly current: KeyframeDragConfig;
}

// --- Controller ---

export class KeyframeDragController {
	private session: Session = { kind: "idle" };
	// Persists through mouseup so the click handler can detect drag vs click
	private mouseDownX: number | null = null;
	private readonly subscribers = new Set<() => void>();
	private readonly configRef: KeyframeDragConfigRef;

	constructor(deps: { configRef: KeyframeDragConfigRef }) {
		this.configRef = deps.configRef;
		this.onKeyframeMouseDown = this.onKeyframeMouseDown.bind(this);
		this.onKeyframeClick = this.onKeyframeClick.bind(this);
		this.getVisualOffsetPx = this.getVisualOffsetPx.bind(this);
		this.handleMouseMove = this.handleMouseMove.bind(this);
		this.handleMouseUp = this.handleMouseUp.bind(this);
	}

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

	get isActive(): boolean {
		return this.session.kind !== "idle";
	}

	get keyframeDragState(): KeyframeDragState {
		if (this.session.kind !== "active") return IDLE_DRAG_STATE;
		return {
			isDragging: true,
			draggingKeyframeIds: new Set(
				this.session.keyframeRefs.map((kf) => kf.keyframeId),
			),
			deltaTicks: this.session.deltaTicks,
		};
	}

	subscribe(fn: () => void): () => void {
		this.subscribers.add(fn);
		return () => this.subscribers.delete(fn);
	}

	cancel(): void {
		this.mouseDownX = null;
		this.finishSession();
	}

	destroy(): void {
		this.deactivate();
		this.subscribers.clear();
	}

	onKeyframeMouseDown({
		event,
		keyframes,
	}: {
		event: ReactMouseEvent;
		keyframes: SelectedKeyframeRef[];
	}): void {
		event.preventDefault();
		event.stopPropagation();

		this.mouseDownX = event.clientX;

		const anySelected = keyframes.some((kf) =>
			this.config.isKeyframeSelected({ keyframe: kf }),
		);
		const isModifierKey = event.shiftKey || event.metaKey || event.ctrlKey;

		if (!anySelected && !isModifierKey) {
			this.config.setKeyframeSelection({ keyframes });
		}

		this.session = {
			kind: "pending",
			keyframeRefs: anySelected ? this.config.selectedKeyframes : keyframes,
			startMouseX: event.clientX,
		};
		this.activate();
		this.notify();
	}

	onKeyframeClick({
		event,
		keyframes,
		orderedKeyframes,
		indicatorTime,
	}: {
		event: ReactMouseEvent;
		keyframes: SelectedKeyframeRef[];
		orderedKeyframes: SelectedKeyframeRef[];
		indicatorTime: number;
	}): void {
		event.stopPropagation();

		const wasDrag =
			this.mouseDownX !== null &&
			Math.abs(event.clientX - this.mouseDownX) > TIMELINE_DRAG_THRESHOLD_PX;
		this.mouseDownX = null;

		if (wasDrag) return;

		const { displayedStartTime, getFps, getTotalDuration, seek } = this.config;
		const fps = getFps();
		const seekTime =
			fps != null
				? (snappedSeekTime({
						time: displayedStartTime + indicatorTime,
						duration: getTotalDuration(),
						rate: fps,
					}) ?? displayedStartTime + indicatorTime)
				: displayedStartTime + indicatorTime;
		seek({ time: seekTime });

		if (event.shiftKey) {
			this.config.selectKeyframeRange({
				orderedKeyframes,
				targetKeyframes: keyframes,
				isAdditive: event.metaKey || event.ctrlKey,
			});
			return;
		}

		this.config.toggleKeyframeSelection({
			keyframes,
			isMultiKey: event.metaKey || event.ctrlKey,
		});
	}

	getVisualOffsetPx({
		indicatorTime,
		indicatorOffsetPx,
		isBeingDragged,
		displayedStartTime,
		elementLeft,
	}: {
		indicatorTime: number;
		indicatorOffsetPx: number;
		isBeingDragged: boolean;
		displayedStartTime: number;
		elementLeft: number;
	}): number {
		if (!isBeingDragged || this.session.kind !== "active")
			return indicatorOffsetPx;
		const clampedTime = Math.max(
			0,
			Math.min(
				this.config.element.duration,
				indicatorTime + this.session.deltaTicks,
			),
		);
		return (
			timelineTimeToSnappedPixels({
				time: displayedStartTime + clampedTime,
				zoomLevel: this.config.zoomLevel,
			}) - elementLeft
		);
	}

	private activate(): void {
		document.addEventListener("mousemove", this.handleMouseMove);
		document.addEventListener("mouseup", this.handleMouseUp);
	}

	private deactivate(): void {
		document.removeEventListener("mousemove", this.handleMouseMove);
		document.removeEventListener("mouseup", this.handleMouseUp);
	}

	private notify(): void {
		for (const fn of this.subscribers) fn();
	}

	private finishSession(): void {
		this.session = { kind: "idle" };
		this.deactivate();
		this.notify();
	}

	private commitDrag({
		keyframeRefs,
		deltaTicks,
	}: {
		keyframeRefs: SelectedKeyframeRef[];
		deltaTicks: number;
	}): void {
		const { element } = this.config;
		const commands: Command[] = keyframeRefs.flatMap((ref) => {
			const keyframe = getKeyframeById({
				animations: element.animations,
				propertyPath: ref.propertyPath,
				keyframeId: ref.keyframeId,
			});
			if (!keyframe) return [];
			return [
				new RetimeKeyframeCommand({
					trackId: ref.trackId,
					elementId: ref.elementId,
					propertyPath: ref.propertyPath,
					keyframeId: ref.keyframeId,
					nextTime: Math.max(
						0,
						Math.min(element.duration, keyframe.time + deltaTicks),
					),
				}),
			];
		});

		const [first, ...rest] = commands;
		if (!first) return;
		if (rest.length === 0) {
			this.config.executeCommand(first);
		} else {
			this.config.executeCommand(new BatchCommand([first, ...rest]));
		}
	}

	private handleMouseMove({ clientX }: MouseEvent): void {
		if (this.session.kind === "pending") {
			const deltaX = Math.abs(clientX - this.session.startMouseX);
			if (deltaX <= TIMELINE_DRAG_THRESHOLD_PX) return;

			this.session = {
				kind: "active",
				keyframeRefs: this.session.keyframeRefs,
				startMouseX: this.session.startMouseX,
				deltaTicks: 0,
			};
			this.notify();
			return;
		}

		if (this.session.kind !== "active") return;

		const fps = this.config.getFps();
		if (!fps) return;

		const pixelsPerSecond =
			BASE_TIMELINE_PIXELS_PER_SECOND * this.config.zoomLevel;
		const rawDeltaTicks = Math.round(
			((clientX - this.session.startMouseX) / pixelsPerSecond) *
				TICKS_PER_SECOND,
		);
		this.session.deltaTicks =
			roundToFrame({ time: rawDeltaTicks, rate: fps }) ?? rawDeltaTicks;
		this.notify();
	}

	private handleMouseUp(): void {
		if (this.session.kind === "pending") {
			this.finishSession();
			return;
		}

		if (this.session.kind !== "active") return;

		const { selectedKeyframes, element } = this.config;
		const { keyframeRefs, deltaTicks } = this.session;
		const draggingIds = new Set(keyframeRefs.map((r) => r.keyframeId));
		const draggingRefs = selectedKeyframes.filter(
			(kf) => kf.elementId === element.id && draggingIds.has(kf.keyframeId),
		);

		if (draggingRefs.length > 0 && deltaTicks !== 0) {
			this.commitDrag({ keyframeRefs: draggingRefs, deltaTicks });
		}

		this.finishSession();
	}
}
