<script lang="ts">
import { crosshairCursor, Decoration, dropCursor, EditorView, keymap, ViewPlugin, ViewUpdate, WidgetType, type DecorationSet } from '@codemirror/view';
import { Annotation, EditorState, RangeValue, SelectionRange, type Range } from '@codemirror/state';
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
import { bracketMatching, defaultHighlightStyle, foldKeymap, HighlightStyle, indentOnInput, syntaxHighlighting, syntaxTree } from '@codemirror/language';
import { search, searchKeymap } from '@codemirror/search';
import { closeBrackets, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete';
import { lintKeymap } from '@codemirror/lint';
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
import { IterMode, Tree } from '@lezer/common';
import { tags } from '@lezer/highlight';
const External = Annotation.define<boolean>();
const Hidden = Decoration.mark({ class: 'hidden' });
const Bullet = Decoration.mark({ class: '*:hidden before:absolute before:top-2 before:left-0 before:inline-block before:w-2 before:h-2 before:rounded before:bg-light-40 dark:before:bg-dark-40 relative ps-4' });
const Blockquote = Decoration.line({ class: '*:hidden before:block !ps-4 relative before:absolute before:top-0 before:bottom-0 before:left-0 before:w-1 before:bg-none before:bg-light-30 dark:before:bg-dark-30' });

const TagTag = tags.special(tags.content);

const intersects = (a: {
    from: number;
    to: number;
}, b: {
    from: number;
    to: number;
}) => !(a.to < b.from || b.to < a.from);

const highlight = HighlightStyle.define([
    { tag: tags.heading1, class: 'text-5xl pt-4 pb-2 after:hidden' },
    { tag: tags.heading2, class: 'text-4xl pt-4 pb-2 ps-1 leading-loose after:hidden' },
    { tag: tags.heading3, class: 'text-2xl font-bold pt-1 after:hidden' },
    { tag: tags.heading4, class: 'text-xl font-semibold pt-1 after:hidden variant-cap' },
    { tag: tags.meta, color: "#404740" },
    { tag: tags.link, textDecoration: "underline" },
    { tag: tags.heading, textDecoration: "underline", fontWeight: "bold" },
    { tag: tags.emphasis, fontStyle: "italic" },
    { tag: tags.strong, fontWeight: "bold" },
    { tag: tags.strikethrough, textDecoration: "line-through" },
    { tag: tags.keyword, color: "#708" },
    { tag: TagTag, class: 'cursor-default bg-accent-blue bg-opacity-10 hover:bg-opacity-20 text-accent-blue text-sm px-1 ms-1 pb-0.5 rounded-full rounded-se-none border border-accent-blue border-opacity-30' }
]);

class Decorator
{
    static hiddenNodes: string[] = [
        'HardBreak',
        'LinkMark',
        'EmphasisMark',
        'CodeMark',
        'CodeInfo',
        'URL',
    ]
    decorations: DecorationSet;
    constructor(view: EditorView)
    {
        this.decorations = Decoration.set(this.iterate(syntaxTree(view.state), view.visibleRanges, []), true);
    }
    update(update: ViewUpdate)
    {
        if(!update.docChanged && !update.viewportChanged && !update.selectionSet)
            return;

        this.decorations = this.decorations.update({
            filter: (f, t, v) => false,
            add: this.iterate(syntaxTree(update.state), update.view.visibleRanges, update.state.selection.ranges),
            sort: true,
        });
    }
    iterate(tree: Tree, visible: readonly {
        from: number;
        to: number;
    }[], selection: readonly SelectionRange[]): Range<Decoration>[]
    {
        const decorations: Range<Decoration>[] = [];
        
        for (let { from, to } of visible) {
            tree.iterate({
                from, to, mode: IterMode.IgnoreMounts,
                enter: node => {
                    if(node.node.parent && selection.some(e => intersects(e, node.node.parent!)))
                        return true;

                    else if(node.name === 'HeaderMark')
                        decorations.push(Hidden.range(node.from, node.to + 1));

                    else if(Decorator.hiddenNodes.includes(node.name))
                        decorations.push(Hidden.range(node.from, node.to));

                    else if(node.matchContext(['BulletList', 'ListItem']) && node.name === 'ListMark')
                        decorations.push(Bullet.range(node.from, node.to + 1));

                    else if(node.matchContext(['Blockquote']))
                        decorations.push(Blockquote.range(node.from, node.from));

                    return true;
                },
            });
        }

        return decorations;
    }
}
</script>

<script setup lang="ts">
const { autofocus = false } = defineProps<{
    placeholder?: string
    autofocus?: boolean
}>();
const model = defineModel<string>();

const editor = useTemplateRef('editor');
const view = ref<EditorView>();

onMounted(() => {
    if(editor.value)
    {
        view.value = new EditorView({
            doc: model.value,
            parent: editor.value,
            extensions: [
                markdown({
                    base: markdownLanguage,
                    extensions: {
                        defineNodes: [
                            { name: "Tag", style: TagTag },
                            { name: "TagMark", style: tags.processingInstruction }
                        ],
                        parseInline: [{
                            name: "Tag",
                            parse(cx, next, pos) {
                                if (next != 35 || cx.char(pos + 1) == 35) return -1;
                                let elts = [cx.elt("TagMark", pos, pos + 1)];
                                for (let i = pos + 1; i < cx.end; i++) {
                                    let next = cx.char(i);
                                    if (next == 35)
                                        return cx.addElement(cx.elt("Tag", pos, i + 1, elts.concat(cx.elt("TagMark", i, i + 1))));
                                    if (next == 92)
                                        elts.push(cx.elt("Escape", i, i++ + 2));
                                    if (next == 32 || next == 9 || next == 10 || next == 13) break;
                                }
                                return -1
                            }
                        }],
                    }
                }),
                history(),
                search(),
                dropCursor(),
                EditorState.allowMultipleSelections.of(true),
                indentOnInput(),
                syntaxHighlighting(highlight),
                bracketMatching(),
                closeBrackets(),
                crosshairCursor(),
                EditorView.lineWrapping,
                keymap.of([
                    ...closeBracketsKeymap,
                    ...defaultKeymap,
                    ...searchKeymap,
                    ...historyKeymap,
                    ...foldKeymap,
                    ...completionKeymap,
                    ...lintKeymap
                ]),
                EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
                    if (viewUpdate.docChanged && !viewUpdate.transactions.some(tr => tr.annotation(External)))
                    {
                        model.value = viewUpdate.state.doc.toString();
                    }
                }),
                EditorView.contentAttributes.of({spellcheck: "true"}),
                ViewPlugin.fromClass(Decorator, {
                    decorations: e => e.decorations,
                })
            ]
        });

        if(autofocus)
        {
            view.value.focus();
        }
    }
});

onBeforeUnmount(() => {
    if (view.value)
    {
        view.value?.destroy();
        view.value = undefined;
    }
});

watchEffect(() => {
    if (model.value === void 0) {
        return;
    }
    const currentValue = view.value ? view.value.state.doc.toString() : "";
    if (view.value && model.value !== currentValue) {
        view.value.dispatch({
            changes: { from: 0, to: currentValue.length, insert: model.value || "" },
            annotations: [External.of(true)],
        });
    }
});

defineExpose({ focus: () => editor.value?.focus() });
</script>

<template>
    <div ref="editor" class="flex flex-1 w-full justify-stretch items-stretch py-2 px-1.5 font-sans text-base"></div>
</template>

<style>
.CodeMirror
{
    @apply bg-transparent;
    @apply flex-1 h-full;
    @apply font-sans;

    @apply text-light-100 dark:text-dark-100;
}
.cancel-gutters .CodeMirror-gutters
{
    @apply hidden;
}
.CodeMirror-sizer
{
    @apply !px-3;
}
.cancel-gutters .CodeMirror-sizer
{
    @apply ms-2;
}
.CodeMirror-gutters
{
    @apply bg-transparent;
    @apply border-transparent;
}
.CodeMirror-gutter-wrapper
{
    @apply absolute top-0 bottom-0;
    @apply flex justify-center items-center;
}
.CodeMirror-foldmarker
{
    @apply text-light-100;
    @apply dark:text-dark-100;
    @apply ps-3;
    text-shadow: none;
}
.hmd-inactive-line .cm-formatting-header, .hmd-inactive-line .cm-formatting-link, .hmd-inactive-line .cm-link-has-alias, .hmd-inactive-line .cm-link-alias-pipe
{
    @apply hidden;
}
.CodeMirror-line 
{
    @apply text-base;
}
.CodeMirror-cursor
{
    @apply border-light-100 dark:border-dark-100;
}
.CodeMirror-selected
{
    @apply bg-light-35 dark:bg-dark-35;
}
.HyperMD-list-line-1 {
    @apply !ps-0;
}
.HyperMD-list-line-2 {
    @apply !ps-6;
}
.HyperMD-list-line-3 {
    @apply !ps-12;
}
</style>