<script lang="ts">
    import { onMount, createEventDispatcher, tick, afterUpdate } from "svelte";
    import { Marked } from "marked";
    import { markedHighlight } from "marked-highlight";
    import hljs from 'highlight.js';
    import "highlight.js/styles/github.css"; // You can choose a different style

    export let value: any;
    export let depth = 0;
    export let is_root = false;
    export let is_last_item = true;
    export let key: string | number | null = null;
    export let open = false;
    export let theme_mode: "system" | "light" | "dark" = "system";
    export let show_indices = false;

    const dispatch = createEventDispatcher();
    let root_element: HTMLElement;
    let collapsed = open ? false : depth >= 3;
    let child_nodes: any[] = [];

    function is_collapsible(val: any): boolean {
        return val !== null && (typeof val === "object" || Array.isArray(val));
    }

    async function toggle_collapse(): Promise<void> {
        collapsed = !collapsed;
        await tick();
        dispatch("toggle", { collapsed, depth });
    }

    function get_collapsed_preview(val: any): string {
        if (Array.isArray(val)) return `Array(${val.length})`;
        if (typeof val === "object" && val !== null)
            return `Object(${Object.keys(val).length})`;
        return String(val);
    }

    const marked = new Marked(
      markedHighlight({
        langPrefix: 'hljs language-',
        highlight(code, lang, info) {
          const language = hljs.getLanguage(lang) ? lang : 'plaintext';
          return hljs.highlight(code, { language }).value;
        }
      })
    );

    function escapeXmlTagsOutsideCodeBlocks(value: string): string {
        const codeBlockRegex = /(```[\s\S]*?```)/g;
        const parts = value.split(codeBlockRegex);
        return parts.map(part => {
            if (part.startsWith('```') && part.endsWith('```')) {
                return part; // Do not escape inside code blocks
            }
            return part.replace(/</g, '&lt;').replace(/>/g, '&gt;'); // Escape outside code blocks
        }).join('');
    }

    function toMarkdown(value: string): string {
        // console.log("Render:" + value);
        const escapedValue = escapeXmlTagsOutsideCodeBlocks(value);
        //console.log("Escaped:" + escapedValue);
        const parsed = marked.parse(escapedValue);
        const parser = new DOMParser();
        const doc = parser.parseFromString(parsed, 'text/html');
        doc.querySelectorAll('code').forEach((codeElement) => {
            codeElement.setAttribute('style', 'border: 1px solid #d3d3d3; background-color: rgba(150,150,150,0.05);');
        });
        return doc.body.innerHTML;
    }

    $: if (is_collapsible(value)) {
        child_nodes = Object.entries(value);
    } else {
        child_nodes = [];
    }
    $: if (is_root && root_element) {
        updateLineNumbers();
    }

    function updateLineNumbers(): void {
        const lines = root_element.querySelectorAll(".line");
        lines.forEach((line, index) => {
            const line_number = line.querySelector(".line-number");
            if (line_number) {
                line_number.setAttribute("data-pseudo-content", (index + 1).toString());
                line_number?.setAttribute(
                    "aria-roledescription",
                    `Line number ${index + 1}`
                );
                line_number?.setAttribute("title", `Line number ${index + 1}`);
            }
        });
    }

    onMount(() => {
        if (is_root) {
            updateLineNumbers();
        }
    });

    afterUpdate(() => {
        if (is_root) {
            updateLineNumbers();
        }
    });
</script>

<div
    class="json-node"
    class:root={is_root}
    class:dark-mode={theme_mode === "dark"}
    bind:this={root_element}
    on:toggle
    style="--depth: {depth};"
>
    <div class="line" class:collapsed>
        <span class="line-number"></span>
        <span class="content">
            {#if is_collapsible(value)}
                <button
                    data-pseudo-content={collapsed ? "▶" : "▼"}
                    aria-label={collapsed ? "Expand" : "Collapse"}
                    class="toggle"
                    on:click={toggle_collapse}
                />
            {/if}

            {#if key !== null}
                <span class="key">"{key}"</span><span class="punctuation colon"
                    >:
                </span>
            {/if}
            {#if is_collapsible(value)}
                <span
                    class="punctuation bracket"
                    class:square-bracket={Array.isArray(value)}
                    >{Array.isArray(value) ? "[" : "{"}</span
                >
                {#if collapsed}
                    <button on:click={toggle_collapse} class="preview">
                        {get_collapsed_preview(value)}
                    </button>
                    <span
                        class="punctuation bracket"
                        class:square-bracket={Array.isArray(value)}
                        >{Array.isArray(value) ? "]" : "}"}</span
                    >
                {/if}
            {:else if typeof value === "string"}
                <span class="value">{@html toMarkdown(value)}</span>
            {:else if typeof value === "number"}
                <span class="value number">{value}</span>
            {:else if typeof value === "boolean"}
                <span class="value bool">{value.toString()}</span>
            {:else if value === null}
                <span class="value null">null</span>
            {:else}
                <span>{value}</span>
            {/if}
            {#if !is_last_item && (!is_collapsible(value) || collapsed)}
                <span class="punctuation">,</span>
            {/if}
        </span>
    </div>

    {#if is_collapsible(value)}
        <div class="children" class:hidden={collapsed}>
            {#each child_nodes as [subKey, subVal], i}
                <svelte:self
                    value={subVal}
                    depth={depth + 1}
                    is_last_item={i === child_nodes.length - 1}
                    key={subKey}
                    {open}
                    {theme_mode}
                    {show_indices}
                    on:toggle
                />
            {/each}
            <div class="line">
                <span class="line-number"></span>
                <span class="content">
                    <span
                        class="punctuation bracket"
                        class:square-bracket={Array.isArray(value)}
                        >{Array.isArray(value) ? "]" : "}"}</span
                    >
                    {#if !is_last_item}<span class="punctuation">,</span>{/if}
                </span>
            </div>
        </div>
    {/if}
</div>

<style>
	.json-node {
		font-family: var(--font-mono);
		--text-color: #d18770;
		--key-color: var(--text-color);
		--string-color: #ce9178;
		--number-color: #719fad;

		--bracket-color: #5d8585;
		--square-bracket-color: #be6069;
		--punctuation-color: #8fbcbb;
		--line-number-color: #6a737d;
		--separator-color: var(--line-number-color);
	}
	.json-node.dark-mode {
		--bracket-color: #7eb4b3;
		--number-color: #638d9a;
	}
	.json-node.root {
		position: relative;
		padding-left: var(--size-14);
	}
	.json-node.root::before {
		content: "";
		position: absolute;
		top: 0;
		bottom: 0;
		left: var(--size-11);
		width: 1px;
		background-color: var(--separator-color);
	}
	.line {
		display: flex;
		align-items: flex-start;
		padding: 0;
		margin: 0;
		line-height: var(--line-md);
	}
	.line-number {
		position: absolute;
		left: 0;
		width: calc(var(--size-7));
		text-align: right;
		color: var(--line-number-color);
		user-select: none;
		text-overflow: ellipsis;
		text-overflow: ellipsis;
		direction: rtl;
		overflow: hidden;
	}
	.content {
		flex: 1;
		display: flex;
		align-items: center;
		padding-left: calc(var(--depth) * var(--size-2));
		flex-wrap: wrap;
	}
	.children {
		padding-left: var(--size-4);
	}
	.children.hidden {
		display: none;
	}
	.key {
		color: var(--key-color);
	}
	.string {
		color: var(--string-color);
	}
	.number {
		color: var(--number-color);
	}
	.bool {
		color: var(--text-color);
	}
	.null {
		color: var(--text-color);
	}
	.value {
		margin-left: var(--spacing-md);
	}
	.punctuation {
		color: var(--punctuation-color);
	}
	.bracket {
		margin-left: var(--spacing-sm);
		color: var(--bracket-color);
	}
	.square-bracket {
		margin-left: var(--spacing-sm);
		color: var(--square-bracket-color);
	}
	.toggle,
	.preview {
		background: none;
		border: none;
		color: inherit;
		cursor: pointer;
		padding: 0;
		margin: 0;
	}
	.toggle {
		user-select: none;
		margin-right: var(--spacing-md);
	}
	.preview {
		margin: 0 var(--spacing-sm) 0 var(--spacing-lg);
	}
	.preview:hover {
		text-decoration: underline;
	}

	:global([data-pseudo-content])::before {
		content: attr(data-pseudo-content);
	}
</style>
