/**
 * Layer 3: Gesture Mapper
 *
 * Resolves an InputEvent against a priority-sorted stack of MappingContexts.
 * Each context is bucketed by pointer gesture type or keyboard phase for O(1) lookup.
 */

import { specificity } from './specificity';
import type {
	Binding,
	InputEvent,
	GuardContext,
	MappingContext,
	ResolvedAction,
} from './types';
import { isKeyInputEvent } from './types';

// =============================================================================
// Index
// =============================================================================

/** Pre-indexed bindings for a single MappingContext. */
export interface ContextIndex {
	contextId: string;
	priority: number;
	enabled: boolean;
	/** Bindings bucketed by gesture type. '__wildcard__' for type-less patterns. */
	buckets: Map<string, Binding[]>;
}

const WILDCARD = '__wildcard__';

function getPatternBucketKey(binding: Binding): string | null {
	const { pattern } = binding;

	if (pattern.type !== undefined) {
		return `pointer:${pattern.type}`;
	}

	if (
		pattern.kind === 'key' ||
		pattern.key !== undefined ||
		pattern.code !== undefined
	) {
		return pattern.phase === 'down' || pattern.phase === 'up'
			? `key:${pattern.phase}`
			: null;
	}

	return null;
}

function getEventBucketKey(event: InputEvent): string {
	return isKeyInputEvent(event)
		? `key:${event.phase}`
		: `pointer:${event.type}`;
}

/**
 * Build a lookup index from a MappingContext.
 * Wildcard bindings (no `type` in pattern) are concatenated into every bucket.
 */
export function indexContext(ctx: MappingContext): ContextIndex {
	const typed = new Map<string, Binding[]>();
	const wildcards: Binding[] = [];

	for (const binding of ctx.bindings) {
		const key = getPatternBucketKey(binding);
		if (key === null) {
			wildcards.push(binding);
		} else {
			let bucket = typed.get(key);
			if (!bucket) {
				bucket = [];
				typed.set(key, bucket);
			}
			bucket.push(binding);
		}
	}

	const buckets = new Map<string, Binding[]>();
	for (const [key, bucket] of typed) {
		buckets.set(key, bucket);
	}
	if (wildcards.length > 0) {
		buckets.set(WILDCARD, wildcards);
	}

	return {
		contextId: ctx.id,
		priority: ctx.priority,
		enabled: ctx.enabled,
		buckets,
	};
}

// =============================================================================
// Stack Index
// =============================================================================

/** The full indexed context stack, sorted by priority ascending. */
export type MappingIndex = ContextIndex[];

/**
 * Build the full mapping index from a list of contexts.
 * Sorts by priority ascending (lower number = higher priority).
 */
export function buildMappingIndex(contexts: MappingContext[]): MappingIndex {
	return contexts
		.map(indexContext)
		.sort((a, b) => a.priority - b.priority);
}

// =============================================================================
// Resolution
// =============================================================================

/**
 * Resolve a gesture event against the indexed context stack.
 *
 * Algorithm:
 * 1. Walk contexts in priority order (ascending = highest priority first).
 * 2. For each enabled context, look up the bucket for event.type.
 * 3. For each binding in the bucket, compute specificity score.
 * 4. If binding has a `when` guard, evaluate it.
 * 5. Track best match per context.
 * 6. If best match has `consumeInput`, return immediately.
 * 7. Otherwise, continue to lower-priority contexts.
 * 8. Return the highest-priority best match across all contexts.
 */
export function resolve(
	event: InputEvent,
	index: MappingIndex,
	guard: GuardContext,
): ResolvedAction | null {
	let winner: ResolvedAction | null = null;

	for (const ctx of index) {
		if (!ctx.enabled) continue;

		const bucket = ctx.buckets.get(getEventBucketKey(event));
		const wildcardBucket = ctx.buckets.get(WILDCARD);
		if (!bucket && !wildcardBucket) continue;

		let bestScore = -1;
		let bestBinding: Binding | null = null;

		for (const binding of [...(bucket ?? []), ...(wildcardBucket ?? [])]) {
			const score = specificity(binding.pattern, event);
			if (score === -1) continue;

			if (binding.when && !binding.when(guard)) continue;

			if (score > bestScore) {
				bestScore = score;
				bestBinding = binding;
			}
		}

		if (bestBinding !== null) {
			const action: ResolvedAction = {
				actionId: bestBinding.actionId,
				binding: bestBinding,
				contextId: ctx.contextId,
				score: bestScore,
				consumed: bestBinding.consumeInput === true,
			};

			if (action.consumed) {
				return action;
			}

			// First (highest-priority) match wins
			if (winner === null) {
				winner = action;
			}
		}
	}

	return winner;
}
