// Camera helpers for bounded-plane (no-wrap) mode.
//
// In no-wrap mode the world is a finite rectangle [0, W) × [0, H).
// The camera must keep the visible viewport inside the world, except
// when the visible viewport is larger than the world along some axis
// — in that case the camera is centred on that axis. This is the
// semantics asserted by the tests in tests/map-no-wrap.test.ts.

import { clamp } from "./math";
import type { Camera, Viewport, World } from "./world";

// minScaleNoWrap returns the smallest camera.scale value at which the
// visible viewport fits inside the world along both axes. Below this
// scale the user would see "void" outside world bounds.
export function minScaleNoWrap(viewport: Viewport, world: World): number {
	return Math.max(viewport.widthPx / world.width, viewport.heightPx / world.height);
}

// clampCameraNoWrap returns a camera whose centre is constrained so
// that the visible viewport stays within world bounds. When the
// visible viewport span exceeds world span on an axis, the camera is
// centred on that axis (independent of input centerX/centerY).
//
// The function does not modify camera.scale. Callers that want to
// also enforce minScaleNoWrap should call that separately.
export function clampCameraNoWrap(camera: Camera, viewport: Viewport, world: World): Camera {
	const halfSpanX = viewport.widthPx / (2 * camera.scale);
	const halfSpanY = viewport.heightPx / (2 * camera.scale);

	let centerX = camera.centerX;
	if (halfSpanX * 2 >= world.width) {
		centerX = world.width / 2;
	} else {
		centerX = clamp(centerX, halfSpanX, world.width - halfSpanX);
	}

	let centerY = camera.centerY;
	if (halfSpanY * 2 >= world.height) {
		centerY = world.height / 2;
	} else {
		centerY = clamp(centerY, halfSpanY, world.height - halfSpanY);
	}

	return { centerX, centerY, scale: camera.scale };
}

// pivotZoom keeps the world point under cursor stable while changing
// camera.scale from oldScale to newScale. It returns a new camera
// with the same scale=newScale and a recomputed centre.
//
// Invariant: screenToWorld(cursorPx, returned, viewport) ===
//            screenToWorld(cursorPx, { ...camera, scale: oldScale }, viewport)
//          (within float64 precision, see tests/map-no-wrap.test.ts).
export function pivotZoom(
	camera: Camera,
	viewport: Viewport,
	cursorPx: { x: number; y: number },
	newScale: number,
): Camera {
	const oldScale = camera.scale;
	if (!(newScale > 0)) {
		throw new Error(`pivotZoom: newScale must be positive, got ${newScale}`);
	}
	const offX = cursorPx.x - viewport.widthPx / 2;
	const offY = cursorPx.y - viewport.heightPx / 2;
	const worldX = camera.centerX + offX / oldScale;
	const worldY = camera.centerY + offY / oldScale;
	return {
		centerX: worldX - offX / newScale,
		centerY: worldY - offY / newScale,
		scale: newScale,
	};
}
