<script setup lang="ts">
/**
 * Props-driven chart display.
 *
 * Renders any of Chart.js's standard chart types via `vue-chartjs`.
 * `chart.js` and `vue-chartjs` are loaded via dynamic `import()` rather
 * than static imports so consumer apps that don't use `ChartDisplay`
 * never have to install them. Vite/Rollup emit a separate chunk for
 * the chart libraries and only fetch it when a chart actually renders.
 *
 * Chart.js is tree-shakable — controllers, scales, elements, and
 * plugins must be registered once before a chart can render. That
 * `Chart.register(...)` call happens lazily inside the async loader,
 * gated by a singleton promise so the registration only runs once
 * per page no matter how many ChartDisplay instances mount.
 *
 * If a 2D canvas context can't be acquired (jsdom, SSR, hardened
 * browsers without canvas), the component renders an inert placeholder
 * canvas instead of triggering the dynamic import — this keeps
 * consumer tests in jsdom-based environments quiet and crash-free.
 */
import type { Component } from "vue"
import type { ChartData, ChartOptions } from "chart.js"
import { computed, defineAsyncComponent } from "vue"
import { cn } from "@/lib/utils"

export type ChartDisplayType =
  | "line"
  | "bar"
  | "pie"
  | "doughnut"
  | "radar"
  | "polarArea"
  | "bubble"
  | "scatter"

export interface ChartDisplayProps {
  chartType?: ChartDisplayType
  data: ChartData
  options?: ChartOptions
  width?: number | string
  height?: number | string
  classList?: string[] | string
}

const props = withDefaults(defineProps<ChartDisplayProps>(), {
  chartType: "line",
  width: "100%",
  height: 300,
})

// Detect whether the host environment can actually paint to a canvas.
// True in real browsers, false in jsdom (where getContext returns null
// AND writes "Not implemented: getContext()" to stderr on every call),
// SSR (no `document`), and hardened browsers with canvas disabled.
//
// We detect jsdom up-front via its userAgent rather than invoking
// `getContext("2d")` to test for support — invoking it would itself
// trigger the noisy warning we're trying to avoid.
const canvasSupported: boolean = (() => {
  if (typeof document === "undefined") {
    return false
  }
  if (typeof navigator !== "undefined" && /jsdom/i.test(navigator.userAgent)) {
    return false
  }
  try {
    return !!document.createElement("canvas").getContext("2d")
  } catch {
    return false
  }
})()

// Singleton promise that loads chart.js + vue-chartjs and registers
// every controller/scale/element/plugin the library exposes. Subsequent
// calls return the same resolved promise — register() runs exactly once.
let chartLibPromise: Promise<typeof import("vue-chartjs")> | null = null

function loadChartLib(): Promise<typeof import("vue-chartjs")> {
  if (!chartLibPromise) {
    chartLibPromise = (async () => {
      const [chartjs, vueChartjs] = await Promise.all([
        import("chart.js"),
        import("vue-chartjs"),
      ])
      chartjs.Chart.register(
        chartjs.CategoryScale,
        chartjs.LinearScale,
        chartjs.RadialLinearScale,
        chartjs.BarElement,
        chartjs.LineElement,
        chartjs.PointElement,
        chartjs.ArcElement,
        chartjs.BarController,
        chartjs.LineController,
        chartjs.PieController,
        chartjs.DoughnutController,
        chartjs.RadarController,
        chartjs.PolarAreaController,
        chartjs.BubbleController,
        chartjs.ScatterController,
        chartjs.Tooltip,
        chartjs.Legend,
        chartjs.Title,
        chartjs.Filler,
      )
      return vueChartjs
    })()
  }
  return chartLibPromise
}

// One defineAsyncComponent per chart type. defineAsyncComponent handles
// the loading state for us (renders nothing until the chunk arrives),
// and the resolved component is cached for subsequent mounts.
const componentMap: Record<ChartDisplayType, Component> = {
  line: defineAsyncComponent(async () => (await loadChartLib()).Line),
  bar: defineAsyncComponent(async () => (await loadChartLib()).Bar),
  pie: defineAsyncComponent(async () => (await loadChartLib()).Pie),
  doughnut: defineAsyncComponent(async () => (await loadChartLib()).Doughnut),
  radar: defineAsyncComponent(async () => (await loadChartLib()).Radar),
  polarArea: defineAsyncComponent(async () => (await loadChartLib()).PolarArea),
  bubble: defineAsyncComponent(async () => (await loadChartLib()).Bubble),
  scatter: defineAsyncComponent(async () => (await loadChartLib()).Scatter),
}

const chartComponent = computed<Component>(
  () => componentMap[props.chartType] ?? componentMap.line,
)

const wrapperClass = computed<string>(() => {
  const cl = props.classList
  const base = "relative"
  if (!cl) {
    return base
  }
  const extra = Array.isArray(cl) ? cl.join(" ") : cl
  return cn(base, extra)
})

const dimensionStyle = computed<Record<string, string>>(() => {
  const toCss = (v: number | string): string =>
    typeof v === "number" ? `${v}px` : v
  return {
    width: toCss(props.width),
    height: toCss(props.height),
  }
})

const mergedOptions = computed<ChartOptions>(() => ({
  responsive: true,
  maintainAspectRatio: false,
  ...(props.options ?? {}),
}))
</script>

<template>
  <div
    :class="wrapperClass"
    :style="dimensionStyle"
    data-testid="chart-display"
    :data-chart-type="chartType"
  >
    <component
      v-if="canvasSupported"
      :is="chartComponent"
      :data="data"
      :options="mergedOptions"
    />
    <!-- Inert placeholder: keeps tests/SSR markup stable when canvas can't paint. -->
    <canvas v-else aria-hidden="true" data-chart-placeholder="true" />
  </div>
</template>
