import type { DirectiveBinding } from 'vue'

interface PatternValue {
	pattern?: RegExp
	transform?: (value: string) => string
}

const DEFAULT_PATTERNS: Record<string, PatternValue> = {
	pint: { pattern: /\d/ },
	int: { pattern: /[\d-]/ },
	pnum: { pattern: /[\d.]/ },
	tel: { pattern: /[\d.\-()\s+]/ },
	money: { pattern: /[\d.\s,]/ },
	number: { pattern: /[\d\-.]/ },
	num: { pattern: /[\d\-.]/ },
	hex: { pattern: /[0-9a-f]/i },
	email: { pattern: /[\w.\-@]/ },
	alpha: { pattern: /[a-z_]/i },
	alphanum: { pattern: /\w/ },
	lower: { pattern: /[a-z]/i, transform: (v: string) => v.toLowerCase() },
	upper: { pattern: /[A-Z]/i, transform: (v: string) => v.toUpperCase() },
	slug: { pattern: /[a-z0-9-]/i, transform: (v: string) => v.toLowerCase().replace(/\s/g, '-') },
	snake: { pattern: /[a-z_]/, transform: (v: string) => v.toLowerCase().replace(/\s/g, '_') },
}

interface PatternState {
	validateOnly?: boolean
	pattern?: RegExp
	modifier?: string
	keydownEvent?: EventListenerOrEventListenerObject
	inputEvent?: EventListenerOrEventListenerObject
	compositionStartEvent?: EventListenerOrEventListenerObject
	compositionEndEvent?: EventListenerOrEventListenerObject
	composing?: boolean
	reformatting?: boolean
}

const stateMap = new WeakMap<HTMLElement, PatternState>()

function getTarget(el: HTMLElement): HTMLInputElement | HTMLTextAreaElement | null {
	if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
		return el
	}
	const target = el.querySelector('input, textarea')
	return target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement ? target : null
}

function getModifiers(binding: DirectiveBinding): string {
	const modifiers = Object.keys(binding.modifiers)
	return modifiers.length > 0 ? modifiers[modifiers.length - 1] : ''
}

function getRegex(state: PatternState): RegExp {
	if (state.pattern != null) { return state.pattern }
	if (state.modifier != null && state.modifier !== '') {
		const presetPattern = (DEFAULT_PATTERNS as Record<string, PatternValue | undefined>)[state.modifier]?.pattern
		return presetPattern ?? /./
	}
	return /./
}

function bindEvents(el: HTMLElement, target: HTMLInputElement | HTMLTextAreaElement, state: PatternState) {
	state.keydownEvent = ((event: KeyboardEvent) => {
		onKeydown(event, target, state)
	}) as EventListener
	state.inputEvent = ((event: InputEvent) => {
		onInput(event, target, state)
	}) as EventListener
	state.compositionStartEvent = (() => {
		onCompositionStart(state)
	}) as EventListener
	state.compositionEndEvent = ((event: CompositionEvent) => {
		onCompositionEnd(event, target, state)
	}) as EventListener

	target.addEventListener('keydown', state.keydownEvent)
	target.addEventListener('input', state.inputEvent, { capture: true })
	target.addEventListener('compositionstart', state.compositionStartEvent)
	target.addEventListener('compositionend', state.compositionEndEvent)
}

function unbindEvents(target: HTMLInputElement | HTMLTextAreaElement, state: PatternState) {
	if (state.keydownEvent) { target.removeEventListener('keydown', state.keydownEvent) }
	if (state.inputEvent) { target.removeEventListener('input', state.inputEvent, { capture: true }) }
	if (state.compositionStartEvent) { target.removeEventListener('compositionstart', state.compositionStartEvent) }
	if (state.compositionEndEvent) { target.removeEventListener('compositionend', state.compositionEndEvent) }

	state.keydownEvent = undefined
	state.inputEvent = undefined
	state.compositionStartEvent = undefined
	state.compositionEndEvent = undefined
}

function isNavigationOrControlKey(event: KeyboardEvent): boolean {
	const allowedKeys = [
		'Tab',
		'Backspace',
		'Delete',
		'ArrowLeft',
		'ArrowRight',
		'ArrowUp',
		'ArrowDown',
		'Enter',
	]
	if (event.ctrlKey || event.altKey || event.metaKey) { return true }
	return allowedKeys.includes(event.key)
}

function onKeydown(event: KeyboardEvent, target: HTMLInputElement | HTMLTextAreaElement, state: PatternState) {
	if (state.composing === true) { return }
	if (isNavigationOrControlKey(event)) { return }

	// If a transform is configured (e.g., slug/snake/lower/upper),
	// do not block keystrokes; normalization happens on input.
	if (state.modifier != null && state.modifier !== '') {
		const transformFn = (DEFAULT_PATTERNS as Record<string, PatternValue | undefined>)[state.modifier]?.transform
		if (typeof transformFn === 'function') { return }
	}

	const regex = getRegex(state)
	const testKey = event.key

	if (!regex.test(testKey)) {
		event.preventDefault()
	}
}

function onCompositionStart(state: PatternState) {
	state.composing = true
}

function onCompositionEnd(
	event: CompositionEvent,
	target: HTMLInputElement | HTMLTextAreaElement,
	state: PatternState,
) {
	state.composing = false
	// Normalize after IME commits text
	onInput(event as unknown as InputEvent, target, state)
}

function sanitizeTransformed(value: string, state: PatternState): string {
	const regex = getRegex(state)
	let transformed: string
	if (state.modifier != null && state.modifier !== '') {
		const transformFn = (DEFAULT_PATTERNS as Record<string, PatternValue | undefined>)[state.modifier]?.transform
		transformed = transformFn ? transformFn(value) : value
	} else {
		transformed = value
	}
	let result = ''
	for (let i = 0; i < transformed.length; i++) {
		const ch = transformed[i]
		if (regex.test(ch)) { result += ch }
	}
	return result
}

function onInput(
	event: InputEvent,
	target: HTMLInputElement | HTMLTextAreaElement,
	state: PatternState,
) {
	if (state.reformatting === true) { return }
	if (state.composing === true) { return }

	const originalValue = target.value
	const selectionEnd = target.selectionEnd ?? originalValue.length

	const left = originalValue.slice(0, selectionEnd)

	const sanitizedFull = sanitizeTransformed(originalValue, state)
	if (sanitizedFull === originalValue) { return }

	const leftSanitized = sanitizeTransformed(left, state)
	const newCursor = leftSanitized.length

	state.reformatting = true
	target.value = sanitizedFull
	target.setSelectionRange(newCursor, newCursor)
	state.reformatting = false
}

// Paste is handled naturally via the input event normalization

const pattern = {
	beforeMount(el: HTMLElement, binding: DirectiveBinding) {
		const target = getTarget(el)
		if (target === null) { return }

		const state: PatternState = {
			modifier: getModifiers(binding),
		}

		if (typeof binding.value === 'object' && binding.value !== null) {
			state.pattern = binding.value.pattern ?? /./
			state.validateOnly = binding.value.validateOnly === true
		}

		stateMap.set(el, state)
		bindEvents(el, target, state)
	},
	updated(el: HTMLElement, binding: DirectiveBinding) {
		const target = getTarget(el)
		if (target === null) { return }

		const state = stateMap.get(el)
		if (state === undefined) { return }

		unbindEvents(target, state)

		state.modifier = getModifiers(binding)

		if (typeof binding.value === 'object' && binding.value !== null) {
			state.pattern = binding.value.pattern ?? /./
			state.validateOnly = binding.value.validateOnly === true
		}

		bindEvents(el, target, state)
	},
	unmounted(el: HTMLElement) {
		const target = getTarget(el)
		const state = stateMap.get(el)

		if (target !== null && state !== undefined) {
			unbindEvents(target, state)
		}

		stateMap.delete(el)
	},
	getSSRProps() {
		return {}
	},
}

export default pattern
