<template>
    <div class="ot-codemirror-wrapper interact-ui">
        <div class="ot-codemirror-toolbar">
            <label for="lang-select">Language:</label>
            <select id="lang-select" v-model="selectedLanguage">
                <option value="javascript">JavaScript</option>
                <option value="html">HTML</option>
                <option value="css">CSS</option>
                <option value="markdown">Markdown</option>
            </select>
        </div>
        <div ref="editorContainer" class="ot-codemirror-editor"></div>
    </div>
</template>

<script setup>
    import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
    import { EditorView, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, rectangularSelection, crosshairCursor, highlightActiveLine, keymap } from '@codemirror/view'
    import { EditorState, Compartment } from '@codemirror/state'
    import { history, defaultKeymap, historyKeymap } from '@codemirror/commands'
    import { foldGutter, indentOnInput, syntaxHighlighting, defaultHighlightStyle, bracketMatching, foldKeymap } from '@codemirror/language'
    import { autocompletion, closeBrackets, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete'
    import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'
    import { lintKeymap } from '@codemirror/lint'
    import { javascript } from '@codemirror/lang-javascript'
    import { html } from '@codemirror/lang-html'
    import { css } from '@codemirror/lang-css'
    import { markdown } from '@codemirror/lang-markdown'

    // ── basicSetup equivalent (R3.AC3) ───────────────────────────────────
    // Constructed inline from individual local ES modules because the local
    // codemirror.js is a UMD bundle, not an ES module.
    const basicSetup = [
        lineNumbers(),
        highlightActiveLineGutter(),
        highlightSpecialChars(),
        history(),
        foldGutter(),
        drawSelection(),
        dropCursor(),
        EditorState.allowMultipleSelections.of(true),
        indentOnInput(),
        syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
        bracketMatching(),
        closeBrackets(),
        autocompletion(),
        rectangularSelection(),
        crosshairCursor(),
        highlightActiveLine(),
        highlightSelectionMatches(),
        keymap.of([
            ...closeBracketsKeymap,
            ...defaultKeymap,
            ...searchKeymap,
            ...historyKeymap,
            ...foldKeymap,
            ...completionKeymap,
            ...lintKeymap,
        ]),
    ]

    // ── Component Identity (R2) ──────────────────────────────────────────
    const componentName = 'ot-codemirror'
    const componentVersion = 'v1.0.0'
    const interfaceVersion = componentVersion

    // ── Props (R2.AC4) ───────────────────────────────────────────────────
    const props = defineProps({
        prop_state: {
            type: Object,
            required: true,
            default: () => ({})
        }
    })

    // ── Internal State ───────────────────────────────────────────────────
    const editorContainer = ref(null)
    let editorView = null
    let langCompartment = null
    let readOnlyCompartment = null
    const selectedLanguage = ref('javascript')
    const currentDoc = ref('')
    let mountComplete = false

    // ── Expose (R2.AC3, R6.AC3) ──────────────────────────────────────────
    defineExpose({
        componentName,
        componentVersion,
        interfaceVersion,
        currentDoc
    })

    // ── Language Map (R4.AC9) ────────────────────────────────────────────
    const LANGUAGE_MAP = {
        javascript: () => javascript(),
        html:       () => html(),
        css:        () => css(),
        markdown:   () => markdown(),
    }

    const SUPPORTED_LANGUAGES = Object.keys(LANGUAGE_MAP)

    // ── Language Resolution Helper (R4.AC5–AC7) ─────────────────────────
    function resolveLanguage(raw) {
        if (raw === undefined || raw === null || typeof raw !== 'string') {
            return 'javascript'  // R4.AC7: default, no warning
        }
        const lower = raw.toLowerCase()
        if (SUPPORTED_LANGUAGES.includes(lower)) {
            return lower  // R4.AC5
        }
        console.warn(`[ot-codemirror] Unrecognised language "${raw}", falling back to javascript`)
        return 'javascript'  // R4.AC6
    }

    // ── CSS Loading Helper (R10) ─────────────────────────────────────────
    function ensureSiteCSS() {
        const href = 'codemirror/site.css'
        // Note: duplicate detection compares the exact href string. If another code path
        // inserts the same stylesheet with a different path form (e.g. an absolute path
        // '/ot-static-content/codemirror/site.css'), this check will not detect it as a
        // duplicate. This is an accepted limitation — within the Owltree runtime, all
        // component code uses the same relative path convention.
        const existing = document.head.querySelector(`link[href="${href}"]`)
        if (existing) return  // R10.AC3: no duplicate

        const link = document.createElement('link')
        link.rel = 'stylesheet'
        link.href = href
        document.head.appendChild(link)  // R10.AC2
        // R10.AC6: no blocking on load — EditorView created regardless
    }

    // ── Autocomplete Placeholder Source (R5) ─────────────────────────────
    /**
     * Placeholder completion source.
     * Replace this function body with real completion logic.
     * Expected return shape:
     *   { from: number, options: [{ label: string, type?: string, info?: string }] }
     *
     * @param {CompletionContext} _context
     * @returns {null}
     */
    function placeholderSource(_context) {
        return null
    }

    // ── Update Listener (R6, R7) ─────────────────────────────────────────
    const updateListener = EditorView.updateListener.of((update) => {
        if (!update.docChanged) return           // R6.AC5: skip non-doc transactions

        const newDoc = update.state.doc.toString()
        currentDoc.value = newDoc                 // R6.AC2: sync ref

        if (!mountComplete) return               // R7.AC6: suppress callback during init

        const onChange = props.prop_state?.onChange
        if (onChange === undefined || onChange === null) return  // R7.AC2
        if (typeof onChange !== 'function') {
            console.warn('[ot-codemirror] prop_state.onChange is not a function:', typeof onChange)
            return                                // R7.AC3
        }
        try {
            onChange(newDoc)                       // R7.AC1, R7.AC4: called after currentDoc updated
        } catch (err) {
            console.error('[ot-codemirror] onChange callback error:', err)  // R7.AC5
        }
    })

    // ── onMounted — Editor Creation (R3, R10) ────────────────────────────
    onMounted(() => {
        // Guard: skip if already mounted (R3.AC9)
        if (editorView) {
            console.warn('[ot-codemirror] EditorView already exists, skipping creation')
            return
        }

        // Guard: skip if container not available (R3.AC7)
        if (!editorContainer.value) {
            console.warn('[ot-codemirror] editorContainer ref not available at mount')
            return
        }

        // CSS loading (R10.AC1–AC4)
        ensureSiteCSS()

        // Resolve initial language (R4.AC4–AC7)
        const resolvedLang = resolveLanguage(props.prop_state?.language)
        selectedLanguage.value = resolvedLang

        // Coerce initialDoc (R3.AC4–AC6)
        const rawDoc = props.prop_state?.initialDoc
        const doc = rawDoc === undefined || rawDoc === null
            ? ''
            : (typeof rawDoc === 'string' ? rawDoc : String(rawDoc))

        // Create compartments
        langCompartment = new Compartment()
        readOnlyCompartment = new Compartment()

        // Resolve read-only (R8.AC1–AC5)
        const isReadOnly = !!props.prop_state?.readOnly
        const readOnlyExts = isReadOnly
            ? [EditorView.editable.of(false), EditorState.readOnly.of(true)]
            : [EditorView.editable.of(true), EditorState.readOnly.of(false)]

        // Create EditorView (R3.AC1, R3.AC3)
        editorView = new EditorView({
            state: EditorState.create({
                doc,
                extensions: [
                    basicSetup,
                    langCompartment.of(LANGUAGE_MAP[resolvedLang]()),
                    readOnlyCompartment.of(readOnlyExts),
                    autocompletion({ override: [placeholderSource] }),
                    updateListener,
                ]
            }),
            parent: editorContainer.value,
        })

        // Sync currentDoc (R6.AC4)
        currentDoc.value = doc

        // Enable onChange callback now that init is complete (R7.AC6)
        mountComplete = true
    })

    // ── onBeforeUnmount — Editor Teardown (R3.AC2, R3.AC8) ──────────────
    onBeforeUnmount(() => {
        if (editorView) {
            editorView.destroy()
            editorView = null
        }
        langCompartment = null
        readOnlyCompartment = null
        mountComplete = false
        // currentDoc retains last value (R6.AC6)
    })

    // ── Watch: Read-Only (R8.AC6) ────────────────────────────────────────
    watch(
        () => props.prop_state?.readOnly,
        (newVal) => {
            if (!editorView || !readOnlyCompartment) return
            const isReadOnly = !!newVal
            editorView.dispatch({
                effects: readOnlyCompartment.reconfigure([
                    EditorView.editable.of(!isReadOnly),
                    EditorState.readOnly.of(isReadOnly),
                ])
            })
        }
    )

    // ── Watch: Language (R4.AC3, R4.AC8) ─────────────────────────────────
    watch(selectedLanguage, (newLang) => {
        if (!editorView || !langCompartment) return
        const ext = LANGUAGE_MAP[newLang]
        if (!ext) return
        editorView.dispatch({
            effects: langCompartment.reconfigure(ext())
        })
    })
</script>

<style scoped>
.ot-codemirror-wrapper {
    display: flex;
    flex-direction: column;
    width: 100%;
    height: 100%;
}

.ot-codemirror-toolbar {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0;
    flex-shrink: 0;
}

.ot-codemirror-editor {
    border: 1px solid #cfcfcf;
    border-radius: 0.25rem;
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.ot-codemirror-editor :deep(.cm-editor) {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
}

.ot-codemirror-editor :deep(.cm-scroller) {
    flex: 1;
    min-height: 0;
    overflow: auto;
}
</style>
