<template>
  <div class="jfn" :class="{ 'jfn--root': depth === 0 }">
    <template v-if="isCollection">
      <div class="jfn-line" @click="toggle">
        <span class="jfn-toggle">{{ expanded ? '▼' : '▶' }}</span>
        <span v-if="label" class="jfn-key">{{ label }}: </span>
        <span class="jfn-brace">{{ openBrace }}</span>
        <span v-if="!expanded" class="jfn-ellipsis">{{ collapsedHint }}</span>
        <span v-if="!expanded" class="jfn-brace">{{ closeBrace }}</span>
      </div>
      <div v-if="expanded" class="jfn-children">
        <JsonFoldNode
          v-for="(entry, idx) in entries"
          :key="entry.key"
          :label="entry.label"
          :value="entry.value"
          :depth="depth + 1"
          :comma="idx < entries.length - 1"
        />
        <div class="jfn-line jfn-close">
          <span class="jfn-brace">{{ closeBrace }}</span>
          <span v-if="comma" class="jfn-comma">,</span>
        </div>
      </div>
    </template>
    <div v-else class="jfn-line jfn-primitive">
      <span v-if="label" class="jfn-key">{{ label }}: </span>
      <span :class="primitiveClass">{{ primitiveText }}</span>
      <span v-if="comma" class="jfn-comma">,</span>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue'
import JsonFoldNode from './JsonFoldNode.vue'

const props = withDefaults(
  defineProps<{
    value: unknown
    label?: string
    depth?: number
    comma?: boolean
  }>(),
  {
    depth: 0,
    comma: false
  }
)

const expanded = ref(props.depth < 1)

const isArray = computed(() => Array.isArray(props.value))
const isObject = computed(
  () => props.value !== null && typeof props.value === 'object' && !isArray.value
)
const isCollection = computed(() => isArray.value || isObject.value)

const openBrace = computed(() => (isArray.value ? '[' : '{'))
const closeBrace = computed(() => (isArray.value ? ']' : '}'))

const entries = computed(() => {
  if (isArray.value) {
    return (props.value as unknown[]).map((v, i) => ({
      key: String(i),
      label: String(i),
      value: v
    }))
  }
  if (isObject.value) {
    return Object.entries(props.value as Record<string, unknown>).map(([k, v]) => ({
      key: k,
      label: JSON.stringify(k),
      value: v
    }))
  }
  return []
})

const collapsedHint = computed(() => {
  const n = entries.value.length
  if (isArray.value) return ` … ${n} items `
  return ` … ${n} keys `
})

const primitiveClass = computed(() => {
  const t = typeof props.value
  if (t === 'string') return 'jfn-val jfn-val--str'
  if (t === 'number') return 'jfn-val jfn-val--num'
  if (t === 'boolean') return 'jfn-val jfn-val--bool'
  return 'jfn-val jfn-val--null'
})

const primitiveText = computed(() => {
  if (props.value === null) return 'null'
  if (props.value === undefined) return 'undefined'
  if (typeof props.value === 'string') return JSON.stringify(props.value)
  return String(props.value)
})

function toggle() {
  expanded.value = !expanded.value
}
</script>

<style scoped>
.jfn {
  font-family: var(--mg-font-mono, Consolas, monospace);
  font-size: 12px;
  line-height: 1.55;
  color: var(--mg-text-light, #e8e0f0);
}

.jfn-line {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 2px;
  cursor: default;
  user-select: text;
}

.jfn-line:not(.jfn-primitive):not(.jfn-close) {
  cursor: pointer;
}

.jfn-toggle {
  display: inline-block;
  width: 14px;
  flex-shrink: 0;
  color: var(--mg-text-muted, #9a8fb0);
  font-size: 10px;
}

.jfn-children {
  padding-left: 16px;
  border-left: 1px solid rgba(255, 255, 255, 0.06);
  margin-left: 6px;
}

.jfn-key {
  color: #c792ea;
}

.jfn-brace {
  color: #89ddff;
}

.jfn-ellipsis {
  color: var(--mg-text-muted, #9a8fb0);
  font-style: italic;
}

.jfn-comma {
  color: var(--mg-text-muted, #9a8fb0);
}

.jfn-val--str {
  color: #c3e88d;
}

.jfn-val--num {
  color: #f78c6c;
}

.jfn-val--bool {
  color: #ffcb6b;
}

.jfn-val--null {
  color: #89ddff;
  font-style: italic;
}
</style>
