/**
 * Inertia Engine
 *
 * Pure momentum simulation with friction decay.
 * No requestAnimationFrame — the caller drives ticks.
 * Fully testable without any DOM or timer dependency.
 */

import type { Vec2 } from './types';

// =============================================================================
// Constants (from current Viewport.tsx)
// =============================================================================

export const PAN_FRICTION = 0.92;
export const ZOOM_FRICTION = 0.88;
export const MIN_VELOCITY = 0.5;
export const ZOOM_SNAP_THRESHOLD = 0.03;
export const VELOCITY_SAMPLE_COUNT = 5;

// =============================================================================
// Velocity Sampler
// =============================================================================

export interface VelocitySample {
	vx: number;
	vy: number;
	t: number;
}

/**
 * Ring buffer that tracks recent velocity samples.
 * Call `sample()` on each move, then `average()` on end.
 */
export class VelocitySampler {
	private samples: VelocitySample[] = [];
	private maxSamples: number;

	constructor(maxSamples = VELOCITY_SAMPLE_COUNT) {
		this.maxSamples = maxSamples;
	}

	sample(vx: number, vy: number, t: number): void {
		if (this.samples.length >= this.maxSamples) {
			this.samples.shift();
		}
		this.samples.push({ vx, vy, t });
	}

	average(): Vec2 {
		const n = this.samples.length;
		if (n === 0) return { x: 0, y: 0 };

		let sx = 0;
		let sy = 0;
		for (const s of this.samples) {
			sx += s.vx;
			sy += s.vy;
		}
		return { x: sx / n, y: sy / n };
	}

	reset(): void {
		this.samples.length = 0;
	}
}

// =============================================================================
// Pan Inertia
// =============================================================================

export class PanInertia {
	private vx: number;
	private vy: number;

	constructor(
		velocity: Vec2,
		private friction = PAN_FRICTION,
		private minVelocity = MIN_VELOCITY,
	) {
		this.vx = velocity.x;
		this.vy = velocity.y;
	}

	/**
	 * Advance one frame. Returns the velocity delta to apply,
	 * or null if below threshold (animation complete).
	 */
	tick(): Vec2 | null {
		this.vx *= this.friction;
		this.vy *= this.friction;

		if (Math.abs(this.vx) < this.minVelocity && Math.abs(this.vy) < this.minVelocity) {
			return null;
		}

		return { x: this.vx, y: this.vy };
	}
}

// =============================================================================
// Zoom Inertia
// =============================================================================

export class ZoomInertia {
	private v: number;

	constructor(
		velocity: number,
		private origin: Vec2,
		private friction = ZOOM_FRICTION,
		private minVelocity = MIN_VELOCITY,
		private snapThreshold = ZOOM_SNAP_THRESHOLD,
	) {
		this.v = velocity;
	}

	/**
	 * Advance one frame. Returns zoom delta + origin,
	 * or null if below threshold.
	 */
	tick(currentZoom: number): { delta: number; origin: Vec2 } | null {
		this.v *= this.friction;

		if (Math.abs(this.v) < this.minVelocity) {
			// Snap to 100% if close
			if (Math.abs(currentZoom - 1) < this.snapThreshold) {
				const snapDelta = 1 - currentZoom;
				return Math.abs(snapDelta) > 0.001
					? { delta: snapDelta, origin: this.origin }
					: null;
			}
			return null;
		}

		return { delta: this.v, origin: this.origin };
	}
}
