<script setup lang="ts">
/**
 * Props-driven icon display.
 *
 * Dynamically resolves a lucide icon by its PascalCase name (kebab-case
 * is normalised). The library is lazy-imported so we only pay for it
 * when an icon actually renders.
 */
import { computed, ref, watch, type Component } from "vue"
import { X } from "@lucide/vue"

export interface IconDisplayProps {
  iconName?: string
  size?: number
  classList?: string[] | string
}

const props = withDefaults(defineProps<IconDisplayProps>(), {
  size: 20,
})

function toPascalCase(input: string): string {
  return input
    .replace(/[-_\s]+(.)?/g, (_, ch?: string) => (ch ? ch.toUpperCase() : ""))
    .replace(/^(.)/, (m) => m.toUpperCase())
}

const resolvedIconName = computed<string>(() => {
  if (!props.iconName) {
    return ""
  }
  return toPascalCase(props.iconName)
})

const rootClass = computed<string>(() => {
  const cl = props.classList
  if (!cl) {
    return ""
  }
  return Array.isArray(cl) ? cl.join(" ") : cl
})

const sizeStyle = computed<Record<string, string>>(() => ({
  width: `${props.size}px`,
  height: `${props.size}px`,
}))

const ResolvedIcon = ref<Component | null>(null)

async function loadIcon(name: string): Promise<void> {
  if (!name) {
    ResolvedIcon.value = null
    return
  }
  const mod = await import("@lucide/vue")
  const registry = mod as unknown as Record<string, Component | undefined>
  ResolvedIcon.value = registry[name] ?? null
}

watch(
  resolvedIconName,
  (next) => {
    void loadIcon(next)
  },
  { immediate: true },
)
</script>

<template>
  <span
    class="inline-flex items-center"
    :class="rootClass"
    :data-icon-name="resolvedIconName"
    data-testid="icon-display"
  >
    <component
      :is="ResolvedIcon"
      v-if="ResolvedIcon"
      :style="sizeStyle"
      :aria-label="resolvedIconName"
    />
    <X
      v-else-if="!resolvedIconName"
      :style="sizeStyle"
      class="text-muted-foreground"
      aria-hidden="true"
      data-testid="icon-display-fallback"
    />
  </span>
</template>
