<script setup lang="ts">
/**
 * BarChart — vertical bars, single or multi-series (grouped or stacked).
 * Renders just the plot; wrap in your own Card for a titled panel.
 *
 *   <BarChart :data="[12,18,9,22]" :labels="['Q1','Q2','Q3','Q4']" />
 *   <BarChart :series="[{name:'A',data:[…]},{name:'B',data:[…]}]" stacked />
 */
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 { resolveColor } from './core/palette'
import { formatLabel, formatValue, type ValueFormat } from './core/format'
import { band, 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
	stacked?: boolean
	/** Soft top→bottom gradient on bars (opt-in; flat by default). */
	gradient?: boolean
	height?: number
	grid?: boolean
	radius?: number
	currency?: string
	prefix?: string
	suffix?: string
	maxLabels?: number
	/** Minimum width per category (px). When the categories need more room than
	    the container has, the chart keeps this width per bar group and scrolls
	    horizontally instead of cramming — handy on mobile. Set 0 to disable. */
	minBarWidth?: number
	animated?: boolean
}>(), {
	height: 220,
	grid: true,
	radius: 4,
	maxLabels: 12,
	minBarWidth: 28,
	animated: true,
})

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

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

// Minimum plot width so dense data scrolls (mobile) instead of cramming.
const minWidth = computed(() =>
	props.minBarWidth > 0 && cats.value.length
		? Math.ceil(cats.value.length * props.minBarWidth + pad.value.left + pad.value.right)
		: 0,
)

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

const stackTotals = computed(() => cats.value.map((_, i) => series.value.reduce((sum, s) => sum + (s.points[i]?.y ?? 0), 0)))
const yMax = computed(() => niceMax(props.stacked ? Math.max(0, ...stackTotals.value) : Math.max(0, ...allValues(series.value))))
const sy = computed(() => linear(0, yMax.value, innerHeight.value, 0))
const yTicks = computed(() => (props.grid ? ticks(0, yMax.value, 4) : []))

const groups = computed(() => band(cats.value.length, innerWidth.value, 0.3))
const subBand = computed(() => {
	const n = props.stacked ? 1 : series.value.length
	const w = groups.value.bandWidth / n
	return { width: w, at: (si: number) => si * w }
})

interface Bar { x: number; y: number; w: number; h: number; color: string; cat: number }
const bars = computed<Bar[]>(() => {
	const out: Bar[] = []
	cats.value.forEach((lbl, i) => {
		const groupStart = groups.value.start(i)
		let stackY = innerHeight.value
		series.value.forEach((s, si) => {
			const v = s.points[i]?.y ?? 0
			const color = resolveColor(s.color, si)
			const full = innerHeight.value - sy.value(v)
			const h = full * progress.value
			const w = subBand.value.width
			const x = props.stacked ? groupStart : groupStart + subBand.value.at(si)
			const y = props.stacked ? (stackY - h) : (innerHeight.value - h)
			if (props.stacked) stackY -= full
			out.push({ x: flipX(x + w / 2) - w / 2, y, w, h, color, cat: i })
		})
	})
	return out
})

const labelStride = computed(() => Math.max(1, Math.ceil(cats.value.length / props.maxLabels)))

// Gradient defs: one per distinct bar color.
const uid = chartUid('bar')
const gradColors = computed(() => [...new Set(bars.value.map(b => b.color))])
const gradIdFor = (color: string) => `${uid}-${gradColors.value.indexOf(color)}`

// ── Shared hover tooltip (mouse-follow, per category) ────────────────────────
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()
	let local = e.clientX - rect.left - pad.value.left
	if (flipX(0) !== 0) local = innerWidth.value - local
	const i = Math.floor((local / Math.max(1, innerWidth.value)) * cats.value.length)
	hoverIdx.value = Math.min(cats.value.length - 1, Math.max(0, i))
	mouseX.value = e.clientX - rect.left
}
function onLeave() { hoverIdx.value = null }

const hoverLabel = computed(() => (hoverIdx.value == null ? '' : formatLabel(cats.value[hoverIdx.value])))
const hoverRows = computed(() => {
	if (hoverIdx.value == null) return []
	return series.value.map((s, si) => ({
		name: s.name,
		color: resolveColor(s.color, si),
		value: formatValue(s.points[hoverIdx.value as number]?.y ?? 0, valueFmt.value),
	}))
})
const hoverTotal = computed(() => {
	if (hoverIdx.value == null || !props.stacked) return null
	return formatValue(stackTotals.value[hoverIdx.value] ?? 0, valueFmt.value)
})
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 class="bgl-chart-scroll w-100p overflow-x">
		<div
			ref="el" class="bgl-chart relative" :style="{ height: `${plotH}px`, minWidth: minWidth ? `${minWidth}px` : undefined }"
			@mousemove="onMove" @mouseleave="onLeave"
		>
			<svg v-if="width" :width="width" :height="plotH" class="display-block overflow-hidden">
				<defs v-if="gradient">
					<linearGradient v-for="c in gradColors" :id="gradIdFor(c)" :key="c" x1="0" y1="0" x2="0" y2="1">
						<stop offset="0%" :stop-color="c" stop-opacity="1" />
						<stop offset="100%" :stop-color="c" stop-opacity="0.55" />
					</linearGradient>
				</defs>
				<g :transform="`translate(${pad.left}, ${pad.top})`">
					<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>

					<rect
						v-for="(b, i) in bars" :key="i"
						:x="b.x" :y="b.y" :width="Math.max(0, b.w)" :height="Math.max(0, b.h)"
						:rx="Math.min(radius, b.w / 2)" :fill="gradient ? `url(#${gradIdFor(b.color)})` : b.color"
						class="bgl-chart__bar" :class="{ 'is-dim opacity-5': hoverIdx != null && hoverIdx !== b.cat }"
					/>

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

			<ChartTooltip
				v-if="hoverIdx != null" :style="tooltipStyle"
				:label="hoverLabel" :rows="hoverRows" :total="hoverTotal"
			/>
		</div>
	</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__bar { transition: opacity 0.15s ease; outline: none; }
.bgl-chart__bar:focus,
.bgl-chart__bar:focus-visible { outline: none; }
[dir="rtl"] .bgl-chart__ytick { text-anchor: start; }
</style>
