<template>
    <div class="json-mapping-tree-viewer">
        <div class="json-tree-toolbar">
            <div class="json-tree-meta">{{ metaText }}</div>
            <div class="json-tree-actions">
                <slot name="actions" />
                <el-button type="primary" plain size="small" :disabled="!rootNode" @click="expandAll">全部展开</el-button>
                <el-button plain size="small" :disabled="!rootNode" @click="collapseAll">全部收起</el-button>
            </div>
        </div>

        <div v-if="rootNode" class="json-tree-body">
            <JsonTreeNode :node="rootNode" :depth="0" :is-last="true" :expanded-keys="expandedKeys" @toggle="toggleNode" />
        </div>
        <pre v-else class="mapping-json-text">{{ source }}</pre>
    </div>
</template>

<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import JsonTreeNode, { type JsonTreeNodeModel, type JsonValueType } from './JsonMappingTreeNode.vue'

defineOptions({
    name: 'JsonMappingTree'
})

type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }
const props = defineProps<{
    source: string
    metaText?: string
}>()

const expandedKeys = ref<Set<string>>(new Set())

const parsedJson = computed<{ valid: true; value: JsonValue } | { valid: false }>(() => {
    try {
        return {
            valid: true,
            value: JSON.parse(props.source) as JsonValue
        }
    } catch {
        return {
            valid: false
        }
    }
})

const rootNode = computed(() => {
    if (!parsedJson.value.valid) return null

    return buildJsonNode(undefined, parsedJson.value.value, '$')
})

watch(
    rootNode,
    node => {
        expandedKeys.value = new Set(node ? collectContainerKeys(node) : [])
    },
    { immediate: true }
)

function buildJsonNode(keyName: string | undefined, source: JsonValue, path: string): JsonTreeNodeModel {
    if (Array.isArray(source)) {
        return {
            id: path,
            keyName,
            openToken: '[',
            closeToken: ']',
            summary: ` ${source.length} 项`,
            children: source.map((item, index) => buildJsonNode(undefined, item, `${path}/${index}`))
        }
    }

    if (source && typeof source === 'object') {
        const entries = Object.entries(source)

        return {
            id: path,
            keyName,
            openToken: '{',
            closeToken: '}',
            summary: ` ${entries.length} 项`,
            children: entries.map(([key, value]) => buildJsonNode(key, value, `${path}/${escapePathKey(key)}`))
        }
    }

    return {
        id: path,
        keyName,
        valueText: formatPrimitiveValue(source),
        valueType: getPrimitiveType(source)
    }
}

function collectContainerKeys(node: JsonTreeNodeModel): string[] {
    if (!node.children) return []

    return [node.id, ...node.children.flatMap(collectContainerKeys)]
}

function escapePathKey(key: string) {
    return key.replace(/~/g, '~0').replace(/\//g, '~1')
}

function formatPrimitiveValue(source: JsonValue) {
    if (typeof source === 'string') return JSON.stringify(source)
    if (source === null) return 'null'
    return String(source)
}

function getPrimitiveType(source: JsonValue): JsonValueType {
    if (source === null) return 'null'
    if (typeof source === 'boolean') return 'boolean'
    if (typeof source === 'number') return 'number'
    return 'string'
}

function toggleNode(id: string) {
    const nextKeys = new Set(expandedKeys.value)

    if (nextKeys.has(id)) {
        nextKeys.delete(id)
    } else {
        nextKeys.add(id)
    }

    expandedKeys.value = nextKeys
}

function expandAll() {
    if (!rootNode.value) return

    expandedKeys.value = new Set(collectContainerKeys(rootNode.value))
}

function collapseAll() {
    expandedKeys.value = new Set()
}
</script>

<style scoped lang="scss">
.json-mapping-tree-viewer {
    display: flex;
    flex: 1;
    flex-direction: column;
    min-height: 0;
}

.json-tree-toolbar {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    min-height: 28px;
    gap: 8px;
    margin-bottom: 8px;
    white-space: nowrap;
}

.json-tree-meta {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    color: #64748b;
    font-size: 13px;
    text-overflow: ellipsis;
}

.json-tree-actions {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
    gap: 8px;
}

.json-tree-body,
.mapping-json-text {
    flex: 1;
    min-height: 0;
    margin: 0;
    padding: 16px;
    border: 1px solid #dbe3f0;
    border-radius: 10px;
    background: #ffffff;
    overflow: auto;
    font-family: Consolas, 'Courier New', monospace;
    font-size: 13px;
    line-height: 1.7;
    color: #172033;
}

.mapping-json-text {
    white-space: pre-wrap;
    word-break: break-word;
}

</style>
