<script setup lang="ts">
import type { PropType } from 'vue'
import type { Language, HighlightTheme } from './CodeTypes'
import { onMounted, ref, computed, watch } from 'vue'
import { useBagelInputShell } from '../bagelInputShell'
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: '' },
	placeholder: { type: String, default: '' },
	height: { type: String, default: '240px' },
	disabled: { type: Boolean, default: false },
	theme: { type: String as PropType<HighlightTheme>, default: 'dark' },
	frame: { type: Boolean, default: undefined },
	outline: { type: Boolean, default: undefined },
	minWidth: { type: String, default: undefined },
	maxWidth: { type: String, default: undefined },
	labelColor: { type: String, default: undefined },
	labelActiveColor: { type: String, default: undefined },
})

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

const { shellClass, shellStyle } = useBagelInputShell(props)

// 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 hasCodeValue = computed(() => Boolean(code.value?.trim()))
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="bagel-input code-editor-container ltr" :class="[shellClass, { 'mb-2': label, 'has-value': hasCodeValue }]"
		:style="[{ height: maxHeight }, shellStyle]"
	>
		<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>
				<pre v-if="placeholder && !hasCodeValue" class="code-placeholder" wrap>{{ placeholder }}</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(--bgl-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-placeholder {
position: absolute;
inset: 0;
margin: 0;
padding: 0;
width: 100%;
height: 100%;
font-family: monospace;
font-size: 1em;
line-height: 1.5;
text-align: left;
pointer-events: none;
opacity: 0.4;
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>
