<script setup lang="ts">
/**
 * LineChart — line + optional area fill, single or multi-series, theme-driven.
 * Renders JUST the plot (no title/card chrome) so you can drop it in any Card.
 *
 *   <LineChart :data="[5,8,6,12]" />
 *   <LineChart :data="revenue" color="green" area :currency="'USD'" />
 *   <LineChart :series="[{ name:'2024', data:[…] }, { name:'2025', data:[…] }]" />
 */
import { computed, ref } from 'vue'
import { chartUid } from './core/uid'
import type { RawPoint, RawSeries } from './core/data'
import { allValues, normalizeSeries, sharedLabels } from './core/data'
import { alpha, resolveColor } from './core/palette'
import { formatLabel, formatValue, type ValueFormat } from './core/format'
import { linear, niceMax, ticks } from './core/useScale'
import { useChartFrame } from './core/useChartFrame'
import { useChartAnim } from './core/useChartAnim'
import ChartTooltip from './ChartTooltip.vue'

const props = withDefaults(defineProps<{
	data?: RawPoint[]
	series?: RawSeries[]
	labels?: (string | number)[]
	color?: string
	/** Fill area under each line. */
	area?: boolean
	/** Use a soft top→bottom gradient for area fills (premium look).
	    Implies `area`. On by default when `area`/`stacked` is set. */
	gradient?: boolean
	/** Stack multi-series areas (implies `area`). Composition over time. */
	stacked?: boolean
	height?: number
	/** Show horizontal grid lines + y ticks. */
	grid?: boolean
	/** Show data-point dots. */
	points?: boolean
	/** Currency code → formats values as currency. */
	currency?: string
	prefix?: string
	suffix?: string
	/** Max x-axis labels to show (avoids crowding). */
	maxLabels?: number
	animated?: boolean
}>(), {
	height: 220,
	grid: true,
	points: false,
	maxLabels: 7,
	animated: true,
})

const el = ref<HTMLElement>()
const heightRef = computed(() => props.height)
const { width, height: plotH, pad, innerWidth, innerHeight, flipX, isRTL } = useChartFrame(el, { height: heightRef })
const { progress } = useChartAnim({ el, enabled: props.animated, duration: 800 })

const series = computed(() => normalizeSeries(props.data, props.series, { labels: props.labels, defaultColor: props.color }))
const cats = computed(() => sharedLabels(series.value))

const valueFmt = computed<ValueFormat>(() => ({ currency: props.currency, prefix: props.prefix, suffix: props.suffix }))
const axisFmt = computed<ValueFormat>(() => ({ ...valueFmt.value, compact: true }))

const isStacked = computed(() => props.stacked && series.value.length > 1)

// Per-label cumulative totals for the stacked domain.
const stackTotals = computed(() => cats.value.map((_, i) => series.value.reduce((sum, s) => sum + (s.points[i]?.y ?? 0), 0)))

const yMax = computed(() => niceMax(isStacked.value ? Math.max(0, ...stackTotals.value) : Math.max(0, ...allValues(series.value))))
const yMin = computed(() => (isStacked.value ? 0 : Math.min(0, ...allValues(series.value))))

const sx = computed(() => linear(0, Math.max(1, cats.value.length - 1), 0, innerWidth.value))
const sy = computed(() => linear(yMin.value, yMax.value, innerHeight.value, 0))

const yTicks = computed(() => (props.grid ? ticks(yMin.value, yMax.value, 4) : []))

const lines = computed(() => {
	const base = innerHeight.value
	// Running cumulative per label for stacking.
	const cum = cats.value.map(() => 0)
	return series.value.map((s, i) => {
		const color = resolveColor(s.color, i)
		if (isStacked.value) {
			const lower = cats.value.map((_, idx) => cum[idx])
			const upper = cats.value.map((_, idx) => cum[idx] + (s.points[idx]?.y ?? 0))
			upper.forEach((v, idx) => { cum[idx] = v })
			const topCoords = upper.map((v, idx) => ({ x: flipX(sx.value(idx)), y: sy.value(v), p: s.points[idx] ?? { label: cats.value[idx], y: v } }))
			const line = topCoords.map((c, idx) => `${idx === 0 ? 'M' : 'L'} ${c.x.toFixed(1)} ${c.y.toFixed(1)}`).join(' ')
			const lowerPts = lower.map((v, idx) => ({ x: flipX(sx.value(idx)), y: sy.value(v) }))
			const down = [...lowerPts].reverse().map(c => `L ${c.x.toFixed(1)} ${c.y.toFixed(1)}`).join(' ')
			const areaPath = topCoords.length ? `${line} ${down} Z` : ''
			return { name: s.name, color, coords: topCoords, line, areaPath }
		}
		const coords = s.points.map((p, idx) => ({ x: flipX(sx.value(idx)), y: sy.value(p.y), p }))
		const line = coords.map((c, idx) => `${idx === 0 ? 'M' : 'L'} ${c.x.toFixed(1)} ${c.y.toFixed(1)}`).join(' ')
		const areaPath = coords.length
			? `${line} L ${coords[coords.length - 1].x.toFixed(1)} ${base} L ${coords[0].x.toFixed(1)} ${base} Z`
			: ''
		return { name: s.name, color, coords, line, areaPath }
	})
})

// In stacked mode the area is always shown (it's the whole point).
const showArea = computed(() => props.area || isStacked.value)
// Gradient fills default ON whenever an area is shown, unless explicitly off.
const useGradient = computed(() => showArea.value && props.gradient !== false)

const uid = chartUid()
const gradId = (i: number) => `${uid}-grad-${i}`

const labelStride = computed(() => Math.max(1, Math.ceil(cats.value.length / props.maxLabels)))
const dashLength = computed(() => (innerWidth.value + innerHeight.value) * 2)
const dashOffset = computed(() => dashLength.value * (1 - progress.value))

// ── Hover crosshair + shared tooltip (follows the mouse across the plot) ──────
const hoverIdx = ref<number | null>(null)
const mouseX = ref(0)

function onMove(e: MouseEvent) {
	const host = el.value
	if (!host || !cats.value.length) return
	const rect = host.getBoundingClientRect()
	// Mouse x within the plot area (account for left padding + RTL flip).
	let local = e.clientX - rect.left - pad.value.left
	if (isRTL.value) local = innerWidth.value - local
	const idx = Math.round((local / Math.max(1, innerWidth.value)) * (cats.value.length - 1))
	hoverIdx.value = Math.min(cats.value.length - 1, Math.max(0, idx))
	mouseX.value = e.clientX - rect.left
}
function onLeave() { hoverIdx.value = null }

const hoverX = computed(() => (hoverIdx.value == null ? 0 : flipX(sx.value(hoverIdx.value))))

// Markers (one per series) at the hovered index.
const hoverMarkers = computed(() => {
	if (hoverIdx.value == null) return []
	return lines.value
		.map(l => l.coords[hoverIdx.value as number])
		.filter(Boolean)
		.map((c, i) => ({ x: c.x, y: c.y, color: lines.value[i].color }))
})

// Tooltip rows: every series' value at the hovered label.
const hoverRows = computed(() => {
	if (hoverIdx.value == null) return []
	return series.value.map((s, i) => ({
		name: s.name,
		color: resolveColor(s.color, i),
		value: formatValue(s.points[hoverIdx.value as number]?.y ?? 0, valueFmt.value),
	}))
})
const hoverLabel = computed(() => (hoverIdx.value == null ? '' : formatLabel(cats.value[hoverIdx.value])))

// Total row for stacked charts.
const hoverTotal = computed(() => {
	if (hoverIdx.value == null || !isStacked.value) return null
	const sum = series.value.reduce((s, srs) => s + (srs.points[hoverIdx.value as number]?.y ?? 0), 0)
	return formatValue(sum, valueFmt.value)
})

// Keep the tooltip inside the chart horizontally.
const tooltipStyle = computed(() => {
	const half = 70
	const x = Math.min(Math.max(mouseX.value, half), Math.max(half, width.value - half))
	return { left: `${x}px` }
})
</script>

<template>
	<div ref="el" class="bgl-chart w-100p relative" :style="{ height: `${plotH}px` }" @mousemove="onMove" @mouseleave="onLeave">
		<svg v-if="width" :width="width" :height="plotH" class="display-block overflow-hidden">
			<defs v-if="useGradient">
				<linearGradient
					v-for="(l, i) in lines" :id="gradId(i)" :key="i"
					gradientUnits="userSpaceOnUse" :x1="0" :y1="0" :x2="0" :y2="innerHeight"
				>
					<stop offset="0%" :stop-color="l.color" :stop-opacity="isStacked ? 0.9 : 0.32" />
					<stop offset="100%" :stop-color="l.color" :stop-opacity="isStacked ? 0.15 : 0.02" />
				</linearGradient>
			</defs>
			<g :transform="`translate(${pad.left}, ${pad.top})`">
				<!-- grid + y ticks -->
				<g v-if="grid" class="bgl-chart__grid">
					<g v-for="(t, i) in yTicks" :key="i">
						<line :x1="0" :x2="innerWidth" :y1="sy(t)" :y2="sy(t)" />
						<text class="bgl-chart__ytick" :x="-8" :y="sy(t) + 3">{{ formatValue(t, axisFmt) }}</text>
					</g>
				</g>

				<!-- area fills (gradient by default; stacked layers read as bands) -->
				<template v-if="showArea">
					<path
						v-for="(l, i) in lines" :key="`a${i}`" :d="l.areaPath"
						:fill="useGradient ? `url(#${gradId(i)})` : alpha(l.color, isStacked ? 78 : 14)"
						stroke="none" :opacity="progress"
					/>
				</template>

				<!-- lines -->
				<path
					v-for="(l, i) in lines" :key="`l${i}`" :d="l.line" :stroke="l.color" stroke-width="2.5"
					fill="none" stroke-linecap="round" stroke-linejoin="round"
					:stroke-dasharray="dashLength" :stroke-dashoffset="dashOffset"
				/>

				<!-- points (values shown via the shared hover tooltip) -->
				<template v-if="points">
					<g v-for="(l, i) in lines" :key="`p${i}`">
						<circle
							v-for="(c, idx) in l.coords" :key="idx"
							:cx="c.x" :cy="c.y" r="3.5" :fill="l.color" class="bgl-chart__pt" :opacity="progress"
						/>
					</g>
				</template>

				<!-- x labels -->
				<g class="bgl-chart__xlabels">
					<text
						v-for="(lbl, i) in cats" v-show="i % labelStride === 0" :key="i"
						:x="flipX(sx(i))" :y="innerHeight + 16" text-anchor="middle"
					>{{ formatLabel(lbl) }}</text>
				</g>

				<!-- hover crosshair + per-series markers -->
				<g v-if="hoverIdx != null" class="bgl-chart__hover" pointer-events="none">
					<line :x1="hoverX" :x2="hoverX" :y1="0" :y2="innerHeight" class="bgl-chart__crosshair" />
					<circle v-for="(m, i) in hoverMarkers" :key="i" :cx="m.x" :cy="m.y" r="4" :fill="m.color" class="bgl-chart__hoverpt" />
				</g>
			</g>
		</svg>

		<!-- shared tooltip following the mouse -->
		<ChartTooltip
			v-if="hoverIdx != null" :style="tooltipStyle"
			:label="hoverLabel" :rows="hoverRows" :total="hoverTotal"
		/>
	</div>
</template>

<style scoped>
.bgl-chart { direction: inherit; }
.bgl-chart__grid line { stroke: var(--bgl-border-color); stroke-width: 1; }
.bgl-chart__ytick,
.bgl-chart__xlabels text { fill: var(--bgl-gray); font-size: 11px; }
.bgl-chart__ytick { text-anchor: end; }
.bgl-chart__pt { cursor: pointer; transition: r 0.15s ease; outline: none; }
.bgl-chart__pt:hover { r: 5; }
.bgl-chart__pt:focus,
.bgl-chart__pt:focus-visible { outline: none; }
[dir="rtl"] .bgl-chart__ytick { text-anchor: start; }

/* hover crosshair + markers */
.bgl-chart__crosshair { stroke: var(--bgl-border-color); stroke-width: 1; stroke-dasharray: 3 3; }
.bgl-chart__hoverpt { stroke: var(--bgl-box-bg); stroke-width: 2; }
</style>
