<script setup lang="ts">
import type { PropType } from 'vue'
import type { Language, HighlightTheme } from './CodeTypes'
import { onMounted, ref, computed, watch } from 'vue'
import { useHighlight } from './useHighlight'

// Props with runtime defaults to avoid undefined at runtime
const props = defineProps({
	language: { type: String as PropType<Language>, default: 'html' },
	readonly: { type: Boolean, default: false },
	modelValue: { type: [String, Object] as PropType<string | Record<string, any>>, default: '' },
	autodetect: { type: Boolean, default: true },
	ignoreIllegals: { type: Boolean, default: true },
	label: { type: String, default: '' },
	height: { type: String, default: '240px' },
	disabled: { type: Boolean, default: false },
	theme: { type: String as PropType<HighlightTheme>, default: 'dark' }
})

const emit = defineEmits(['update:modelValue'])

// Track whether the original modelValue was an object
const isObjectMode = computed(() => typeof props.modelValue === 'object')

// Helper to convert value to string for display
function valueToString(value: string | Record<string, any>): string {
	if (typeof value === 'string') return value
	if (typeof value === 'object') {
		return JSON.stringify(value, null, 2)
	}
	return ''
}

// State
const code = ref(valueToString(props.modelValue))
const editorRef = ref<HTMLDivElement>()
const isFocused = ref(false)
const { loaded, loadHighlight, highlightCode, setTheme } = useHighlight(props.theme)

const maxHeight = computed(() => {
	const h = props.height
	return h.match(/^\d+$/) ? `${h}px` : h
})

const formattedCode = computed(() => {
	return highlightCode(
		code.value,
		props.language,
		props.autodetect,
		props.ignoreIllegals
	)
})

function handleInput(e: Event) {
	const target = e.target as HTMLTextAreaElement
	code.value = target.value

	// If originally an object, try to parse and emit object
	if (isObjectMode.value) {
		try {
			const parsed = JSON.parse(code.value)
			emit('update:modelValue', parsed)
		} catch {
			// If parsing fails, emit the string as-is (user is still typing)
			// This allows intermediate invalid JSON states while editing
			emit('update:modelValue', code.value)
		}
	} else {
		emit('update:modelValue', code.value)
	}
}

function handleTab(event: KeyboardEvent) {
	if (event.key !== 'Tab') { return }

	event.preventDefault()
	const target = event.target as HTMLTextAreaElement
	const start = target.selectionStart
	const end = target.selectionEnd

	// Add tab or indent selected text
	const newValue = `${code.value.substring(0, start)}  ${code.value.substring(end)}`
	code.value = newValue

	// If originally an object, try to parse and emit object
	if (isObjectMode.value) {
		try {
			const parsed = JSON.parse(code.value)
			emit('update:modelValue', parsed)
		} catch {
			// If parsing fails, emit the string as-is
			emit('update:modelValue', code.value)
		}
	} else {
		emit('update:modelValue', code.value)
	}

	// Move cursor position after the inserted tab
	setTimeout(() => {
		target.selectionStart = start + 2
		target.selectionEnd = start + 2
	}, 0)
}

// Lifecycle
onMounted(async () => {
	await loadHighlight()
})

watch(() => props.theme, (t) => { setTheme(t) })
function syncFromModel() {
	const newCodeStr = valueToString(props.modelValue)
	if (newCodeStr !== code.value) {
		code.value = newCodeStr
	}
}

function handleBlur() {
	isFocused.value = false
	syncFromModel()
}

watch(() => props.modelValue, () => {
	// Don't overwrite user edits when focused (avoids sync race with parent form state)
	if (isFocused.value) return
	syncFromModel()
}, { immediate: true })
</script>

<template>
	<div class="code-editor-container ltr" :style="{ height: maxHeight }" :class="{ 'mb-2': label }">
		<label v-if="label" class="label">{{ label }}</label>
		<div v-if="loaded" ref="editorRef" class="code-editor-grandpa hljs">
			<div class="editor-content-papa relative">
				<pre class="code-display" wrap><code v-html="formattedCode" /></pre>
				<textarea
					v-if="!readonly && !disabled" :value="code" class="code-input" spellcheck="false"
					autocomplete="off" autocorrect="off" autocapitalize="off" @input="handleInput"
					@keydown="handleTab"
					@focus="isFocused = true"
					@blur="handleBlur"
				/>
			</div>
		</div>
	</div>
</template>

<style scoped>
.code-editor-container {
	margin-bottom: 0.5rem;
	height: 100%;
}

.label {
	display: block;
	text-align: left;
	margin-bottom: 0.25rem;
}

.code-editor-grandpa {
	border-radius: var(--code-border-radius, 0.25rem);
	width: 100%;
	height: 100%;
	overflow: auto;
	padding: 1ch;
	padding-inline-start: 2ch;
}

.code-editor-grandpa:focus-within {
	outline: solid 1px var(--border-color, #4f575f);
	box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.13);
}

.editor-content-papa {
	position: relative;
	width: 100%;
		min-height: 100%;
}

.code-display,
.code-input {
	inset: 0;
	margin: 0;
	padding: 0;
	width: 100%;
	height: 100%;
	font-family: monospace;
	font-size: 1em;
	line-height: 1.5;
	tab-size: 2;
	word-break: keep-all;
	text-align: left;
}

.code-display {
	position: relative;
	pointer-events: none;
	z-index: 1;
}

.code-display code {
	display: block;
	background: transparent !important;
	padding: 0 !important;
}

.code-input {
	position: absolute;
	background: transparent;
	color: transparent;
	caret-color: #fff;
	border: none;
	resize: none;
	outline: none;
	z-index: 2;
}

.code-input::selection {
	background-color: rgba(36, 102, 188, 0.3);
	color: transparent;
}
</style>

<style>
/* Global styles */
pre code.hljs {
	padding: 0 !important;
	background: transparent !important;
}
</style>
