export interface SnapLine {
	type: "horizontal" | "vertical";
	position: number;
}

const ROTATION_SNAP_STEP_DEGREES = 90;
const ROTATION_SNAP_THRESHOLD_DEGREES = 5;
export const MIN_SCALE = 0.01;
export const SNAP_THRESHOLD_SCREEN_PIXELS = 8;

export interface SnapResult {
	snappedPosition: { x: number; y: number };
	activeLines: SnapLine[];
}

type ScaleEdge = "left" | "right" | "top" | "bottom";

export interface ScaleEdgePreference {
	left?: boolean;
	right?: boolean;
	top?: boolean;
	bottom?: boolean;
}

function hasPreferredEdge({
	preferredEdges,
	edge,
}: {
	preferredEdges?: ScaleEdgePreference;
	edge: ScaleEdge;
}): boolean {
	return preferredEdges?.[edge] === true;
}

function pickClosestScaleCandidate<T extends { distance: number; edge: ScaleEdge }>({
	candidates,
	preferredEdges,
}: {
	candidates: T[];
	preferredEdges?: ScaleEdgePreference;
}): T | null {
	if (candidates.length === 0) {
		return null;
	}

	return candidates.reduce((bestCandidate, candidate) => {
		if (candidate.distance < bestCandidate.distance) {
			return candidate;
		}
		if (candidate.distance > bestCandidate.distance) {
			return bestCandidate;
		}

		const shouldPreferCandidate = hasPreferredEdge({
			preferredEdges,
			edge: candidate.edge,
		});
		const shouldPreferBestCandidate = hasPreferredEdge({
			preferredEdges,
			edge: bestCandidate.edge,
		});

		return shouldPreferCandidate && !shouldPreferBestCandidate
			? candidate
			: bestCandidate;
	});
}

export function snapPosition({
	proposedPosition,
	canvasSize,
	elementSize,
	rotation = 0,
	snapThreshold,
}: {
	proposedPosition: { x: number; y: number };
	canvasSize: { width: number; height: number };
	elementSize: { width: number; height: number };
	rotation?: number;
	snapThreshold: { x: number; y: number };
}): SnapResult {
	const centerX = 0;
	const centerY = 0;
	const left = -canvasSize.width / 2;
	const right = canvasSize.width / 2;
	const top = -canvasSize.height / 2;
	const bottom = canvasSize.height / 2;

	const rotRad = (rotation * Math.PI) / 180;
	const cosR = Math.abs(Math.cos(rotRad));
	const sinR = Math.abs(Math.sin(rotRad));
	const halfWidth = (elementSize.width * cosR + elementSize.height * sinR) / 2;
	const halfHeight = (elementSize.width * sinR + elementSize.height * cosR) / 2;
	const activeLines: SnapLine[] = [];

	type AxisSnapCandidate = {
		snappedPosition: number;
		line: SnapLine;
		distance: number;
	};

	function getClosestAxisSnap({
		candidates,
		threshold,
	}: {
		candidates: AxisSnapCandidate[];
		threshold: number;
	}): AxisSnapCandidate | null {
		const snapCandidatesWithinThreshold = candidates.filter(
			(candidate) => candidate.distance <= threshold,
		);
		if (snapCandidatesWithinThreshold.length === 0) {
			return null;
		}
		return snapCandidatesWithinThreshold.reduce((closest, current) =>
			current.distance < closest.distance ? current : closest,
		);
	}

	const verticalTargets = [centerX, left, right];
	const horizontalTargets = [centerY, top, bottom];

	const xCandidates: AxisSnapCandidate[] = [];
	for (const targetX of verticalTargets) {
		xCandidates.push({
			snappedPosition: targetX,
			line: { type: "vertical", position: targetX },
			distance: Math.abs(proposedPosition.x - targetX),
		});
		xCandidates.push({
			snappedPosition: targetX + halfWidth,
			line: { type: "vertical", position: targetX },
			distance: Math.abs(proposedPosition.x - halfWidth - targetX),
		});
		xCandidates.push({
			snappedPosition: targetX - halfWidth,
			line: { type: "vertical", position: targetX },
			distance: Math.abs(proposedPosition.x + halfWidth - targetX),
		});
	}
	const yCandidates: AxisSnapCandidate[] = [];
	for (const targetY of horizontalTargets) {
		yCandidates.push({
			snappedPosition: targetY,
			line: { type: "horizontal", position: targetY },
			distance: Math.abs(proposedPosition.y - targetY),
		});
		yCandidates.push({
			snappedPosition: targetY + halfHeight,
			line: { type: "horizontal", position: targetY },
			distance: Math.abs(proposedPosition.y - halfHeight - targetY),
		});
		yCandidates.push({
			snappedPosition: targetY - halfHeight,
			line: { type: "horizontal", position: targetY },
			distance: Math.abs(proposedPosition.y + halfHeight - targetY),
		});
	}

	const closestX = getClosestAxisSnap({
		candidates: xCandidates,
		threshold: snapThreshold.x,
	});
	const closestY = getClosestAxisSnap({
		candidates: yCandidates,
		threshold: snapThreshold.y,
	});

	const x = closestX?.snappedPosition ?? proposedPosition.x;
	const y = closestY?.snappedPosition ?? proposedPosition.y;
	if (closestX) {
		activeLines.push(closestX.line);
	}
	if (closestY) {
		activeLines.push(closestY.line);
	}

	return {
		snappedPosition: { x, y },
		activeLines,
	};
}

export interface ScaleSnapResult {
	snappedScale: number;
	activeLines: SnapLine[];
}

export function snapScale({
	proposedScale,
	position,
	baseWidth,
	baseHeight,
	rotation = 0,
	canvasSize,
	snapThreshold,
	preferredEdges,
}: {
	proposedScale: number;
	position: { x: number; y: number };
	baseWidth: number;
	baseHeight: number;
	rotation?: number;
	canvasSize: { width: number; height: number };
	snapThreshold: { x: number; y: number };
	preferredEdges?: ScaleEdgePreference;
}): ScaleSnapResult {
	const centerX = 0;
	const centerY = 0;
	const left = -canvasSize.width / 2;
	const right = canvasSize.width / 2;
	const top = -canvasSize.height / 2;
	const bottom = canvasSize.height / 2;

	const rotRad = (rotation * Math.PI) / 180;
	const cosR = Math.abs(Math.cos(rotRad));
	const sinR = Math.abs(Math.sin(rotRad));
	const aabbBaseHalfW = (baseWidth * cosR + baseHeight * sinR) / 2;
	const aabbBaseHalfH = (baseWidth * sinR + baseHeight * cosR) / 2;

	const leftEdge = position.x - aabbBaseHalfW * proposedScale;
	const rightEdge = position.x + aabbBaseHalfW * proposedScale;
	const topEdge = position.y - aabbBaseHalfH * proposedScale;
	const bottomEdge = position.y + aabbBaseHalfH * proposedScale;

	interface SnapCandidate {
		scale: number;
		distance: number;
		lines: SnapLine[];
		edge: ScaleEdge;
	}

	const candidates: SnapCandidate[] = [];

	const verticalTargets = [
		{ position: left, line: { type: "vertical" as const, position: left } },
		{
			position: centerX,
			line: { type: "vertical" as const, position: centerX },
		},
		{ position: right, line: { type: "vertical" as const, position: right } },
	];

	for (const target of verticalTargets) {
		const distanceLeft = Math.abs(leftEdge - target.position);
		if (distanceLeft <= snapThreshold.x) {
			const scale = (position.x - target.position) / aabbBaseHalfW;
			if (Math.abs(scale) > MIN_SCALE) {
				candidates.push({
					scale,
					distance: distanceLeft,
					lines: [target.line],
					edge: "left",
				});
			}
		}
		const distanceRight = Math.abs(rightEdge - target.position);
		if (distanceRight <= snapThreshold.x) {
			const scale = (target.position - position.x) / aabbBaseHalfW;
			if (Math.abs(scale) > MIN_SCALE) {
				candidates.push({
					scale,
					distance: distanceRight,
					lines: [target.line],
					edge: "right",
				});
			}
		}
	}

	const horizontalTargets = [
		{ position: top, line: { type: "horizontal" as const, position: top } },
		{
			position: centerY,
			line: { type: "horizontal" as const, position: centerY },
		},
		{
			position: bottom,
			line: { type: "horizontal" as const, position: bottom },
		},
	];

	for (const target of horizontalTargets) {
		const distanceTop = Math.abs(topEdge - target.position);
		if (distanceTop <= snapThreshold.y) {
			const scale = (position.y - target.position) / aabbBaseHalfH;
			if (Math.abs(scale) > MIN_SCALE) {
				candidates.push({
					scale,
					distance: distanceTop,
					lines: [target.line],
					edge: "top",
				});
			}
		}
		const distanceBottom = Math.abs(bottomEdge - target.position);
		if (distanceBottom <= snapThreshold.y) {
			const scale = (target.position - position.y) / aabbBaseHalfH;
			if (Math.abs(scale) > MIN_SCALE) {
				candidates.push({
					scale,
					distance: distanceBottom,
					lines: [target.line],
					edge: "bottom",
				});
			}
		}
	}

	const best = pickClosestScaleCandidate({
		candidates,
		preferredEdges,
	});
	if (!best) {
		return { snappedScale: proposedScale, activeLines: [] };
	}

	const snappedLeft = position.x - aabbBaseHalfW * best.scale;
	const snappedRight = position.x + aabbBaseHalfW * best.scale;
	const snappedTop = position.y - aabbBaseHalfH * best.scale;
	const snappedBottom = position.y + aabbBaseHalfH * best.scale;

	const activeLines: SnapLine[] = [];
	const seenKeys = new Set<string>();

	function addLine({ line }: { line: SnapLine }) {
		const key = `${line.type}-${line.position}`;
		if (!seenKeys.has(key)) {
			seenKeys.add(key);
			activeLines.push(line);
		}
	}

	for (const target of verticalTargets) {
		if (
			(hasPreferredEdge({ preferredEdges, edge: "left" }) &&
				Math.abs(snappedLeft - target.position) <= 1) ||
			(hasPreferredEdge({ preferredEdges, edge: "right" }) &&
				Math.abs(snappedRight - target.position) <= 1) ||
			(!preferredEdges &&
				(Math.abs(snappedLeft - target.position) <= 1 ||
					Math.abs(snappedRight - target.position) <= 1))
		) {
			addLine({ line: target.line });
		}
	}
	for (const target of horizontalTargets) {
		if (
			(hasPreferredEdge({ preferredEdges, edge: "top" }) &&
				Math.abs(snappedTop - target.position) <= 1) ||
			(hasPreferredEdge({ preferredEdges, edge: "bottom" }) &&
				Math.abs(snappedBottom - target.position) <= 1) ||
			(!preferredEdges &&
				(Math.abs(snappedTop - target.position) <= 1 ||
					Math.abs(snappedBottom - target.position) <= 1))
		) {
			addLine({ line: target.line });
		}
	}

	return {
		snappedScale: best.scale,
		activeLines,
	};
}

export interface AxisSnapResult {
	snappedScale: number;
	/** Infinity when no snap candidate was within threshold */
	snapDistance: number;
	activeLines: SnapLine[];
}

export function snapScaleAxes({
	proposedScaleX,
	proposedScaleY,
	position,
	baseWidth,
	baseHeight,
	rotation = 0,
	canvasSize,
	snapThreshold,
	preferredEdges,
}: {
	proposedScaleX: number;
	proposedScaleY: number;
	position: { x: number; y: number };
	baseWidth: number;
	baseHeight: number;
	rotation?: number;
	canvasSize: { width: number; height: number };
	snapThreshold: { x: number; y: number };
	preferredEdges?: ScaleEdgePreference;
}): { x: AxisSnapResult; y: AxisSnapResult } {
	const canvasLeft = -canvasSize.width / 2;
	const canvasRight = canvasSize.width / 2;
	const canvasTop = -canvasSize.height / 2;
	const canvasBottom = canvasSize.height / 2;

	const rotRad = (rotation * Math.PI) / 180;
	const cosR = Math.abs(Math.cos(rotRad));
	const sinR = Math.abs(Math.sin(rotRad));
	const EPSILON = 1e-6;

	// Current AABB edges at proposed scales
	const currentAabbHalfW = (baseWidth * proposedScaleX * cosR + baseHeight * proposedScaleY * sinR) / 2;
	const currentAabbHalfH = (baseWidth * proposedScaleX * sinR + baseHeight * proposedScaleY * cosR) / 2;
	const currentLeftEdge = position.x - currentAabbHalfW;
	const currentRightEdge = position.x + currentAabbHalfW;
	const currentTopEdge = position.y - currentAabbHalfH;
	const currentBottomEdge = position.y + currentAabbHalfH;

	interface Candidate {
		scale: number;
		distance: number;
		line: SnapLine;
		edge: ScaleEdge;
	}

	function bestCandidate({
		candidates,
		proposedScale,
	}: {
		candidates: Candidate[];
		proposedScale: number;
	}): AxisSnapResult {
		const best = pickClosestScaleCandidate({
			candidates,
			preferredEdges,
		});
		if (!best) {
			return { snappedScale: proposedScale, snapDistance: Infinity, activeLines: [] };
		}
		return { snappedScale: best.scale, snapDistance: best.distance, activeLines: [best.line] };
	}

	// sX candidates: snap via vertical targets (left/right AABB edges) — only valid when cosR ≠ 0
	// snap via horizontal targets (top/bottom AABB edges) — only valid when sinR ≠ 0
	const xCandidates: Candidate[] = [];
	const yContribW = baseHeight * proposedScaleY * sinR;
	const yContribH = baseHeight * proposedScaleY * cosR;

	if (cosR > EPSILON) {
		for (const T of [canvasLeft, 0, canvasRight]) {
			const line: SnapLine = { type: "vertical", position: T };
			const distLeft = Math.abs(currentLeftEdge - T);
			if (distLeft <= snapThreshold.x) {
				const scale = (2 * (position.x - T) - yContribW) / (baseWidth * cosR);
				if (Math.abs(scale) > MIN_SCALE) xCandidates.push({ scale, distance: distLeft, line, edge: "left" });
			}
			const distRight = Math.abs(currentRightEdge - T);
			if (distRight <= snapThreshold.x) {
				const scale = (2 * (T - position.x) - yContribW) / (baseWidth * cosR);
				if (Math.abs(scale) > MIN_SCALE) xCandidates.push({ scale, distance: distRight, line, edge: "right" });
			}
		}
	}

	if (sinR > EPSILON) {
		for (const T of [canvasTop, 0, canvasBottom]) {
			const line: SnapLine = { type: "horizontal", position: T };
			const distTop = Math.abs(currentTopEdge - T);
			if (distTop <= snapThreshold.y) {
				const scale = (2 * (position.y - T) - yContribH) / (baseWidth * sinR);
				if (Math.abs(scale) > MIN_SCALE) xCandidates.push({ scale, distance: distTop, line, edge: "top" });
			}
			const distBottom = Math.abs(currentBottomEdge - T);
			if (distBottom <= snapThreshold.y) {
				const scale = (2 * (T - position.y) - yContribH) / (baseWidth * sinR);
				if (Math.abs(scale) > MIN_SCALE) xCandidates.push({ scale, distance: distBottom, line, edge: "bottom" });
			}
		}
	}

	// sY candidates: snap via vertical targets — only valid when sinR ≠ 0
	// snap via horizontal targets — only valid when cosR ≠ 0
	const yCandidates: Candidate[] = [];
	const xContribW = baseWidth * proposedScaleX * cosR;
	const xContribH = baseWidth * proposedScaleX * sinR;

	if (sinR > EPSILON) {
		for (const T of [canvasLeft, 0, canvasRight]) {
			const line: SnapLine = { type: "vertical", position: T };
			const distLeft = Math.abs(currentLeftEdge - T);
			if (distLeft <= snapThreshold.x) {
				const scale = (2 * (position.x - T) - xContribW) / (baseHeight * sinR);
				if (Math.abs(scale) > MIN_SCALE) yCandidates.push({ scale, distance: distLeft, line, edge: "left" });
			}
			const distRight = Math.abs(currentRightEdge - T);
			if (distRight <= snapThreshold.x) {
				const scale = (2 * (T - position.x) - xContribW) / (baseHeight * sinR);
				if (Math.abs(scale) > MIN_SCALE) yCandidates.push({ scale, distance: distRight, line, edge: "right" });
			}
		}
	}

	if (cosR > EPSILON) {
		for (const T of [canvasTop, 0, canvasBottom]) {
			const line: SnapLine = { type: "horizontal", position: T };
			const distTop = Math.abs(currentTopEdge - T);
			if (distTop <= snapThreshold.y) {
				const scale = (2 * (position.y - T) - xContribH) / (baseHeight * cosR);
				if (Math.abs(scale) > MIN_SCALE) yCandidates.push({ scale, distance: distTop, line, edge: "top" });
			}
			const distBottom = Math.abs(currentBottomEdge - T);
			if (distBottom <= snapThreshold.y) {
				const scale = (2 * (T - position.y) - xContribH) / (baseHeight * cosR);
				if (Math.abs(scale) > MIN_SCALE) yCandidates.push({ scale, distance: distBottom, line, edge: "bottom" });
			}
		}
	}

	return {
		x: bestCandidate({ candidates: xCandidates, proposedScale: proposedScaleX }),
		y: bestCandidate({ candidates: yCandidates, proposedScale: proposedScaleY }),
	};
}

export interface RotationSnapResult {
	snappedRotation: number;
	isSnapped: boolean;
}

export function snapRotation({
	proposedRotation,
}: {
	proposedRotation: number;
}): RotationSnapResult {
	const nearestRotationSnap =
		Math.round(proposedRotation / ROTATION_SNAP_STEP_DEGREES) *
		ROTATION_SNAP_STEP_DEGREES;
	const distanceToNearestSnap = Math.abs(
		proposedRotation - nearestRotationSnap,
	);
	if (distanceToNearestSnap <= ROTATION_SNAP_THRESHOLD_DEGREES) {
		return { snappedRotation: nearestRotationSnap, isSnapped: true };
	}
	return { snappedRotation: proposedRotation, isSnapped: false };
}
