<script setup lang="ts">
defineOptions({ name: 'BglImage', inheritAttrs: true })
import { Skeleton, normalizeDimension, Icon, useImageSrc } from '@bagelink/vue'
import { Comment, computed, useSlots, type StyleValue, type VNode } from 'vue'

interface ImageProps {
	src?: string
	pathKey?: string
	alt?: string
	width?: string | number
	height?: string | number
	caption?: string
	modelValue?: string
	/** Aspect ratio, e.g. "1", "2-3", "16-9" — maps to the `ratio-*` utility (keeps a stable box while loading). */
	ratio?: string
	/** Gradient shown behind/instead of the image — a tone (`purple`), a CSS gradient string, or `[from, to]`. Doubles as the fallback when the image fails or is absent. */
	gradient?: string | [string, string]
	/** object-fit for the image. @default 'cover' when `ratio`/`gradient` is set. */
	fit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'
	/** mix-blend-mode for blending the photo into the gradient (e.g. 'luminosity'). */
	blend?: string
	/** Rounded corners — true (default radius) or a px radius. */
	rounded?: boolean | number
}

const props = defineProps<ImageProps>()

const { imageSrc, loadingError } = useImageSrc(() => props.src ?? props.pathKey ?? props.modelValue)

// ── Framed mode (ratio / gradient / blend / rounded / overlay slot) ──────────
const slots = useSlots()
// Only count the default slot as "framed" when it actually renders nodes — an
// empty/whitespace slot (or none) must NOT force framed mode, otherwise a plain
// `<Image :src />` renders an absolute-fill img that collapses the box to 0×0.
const hasOverlay = computed(() => {
	const nodes = slots.default?.({}) as VNode[] | undefined
	return !!nodes?.some((n: VNode) => n.type !== Comment && !(typeof n.children === 'string' && n.children.trim() === ''))
})
const framed = computed(() =>
	props.ratio !== undefined || props.gradient !== undefined || props.blend !== undefined
	|| props.rounded !== undefined || hasOverlay.value)

// Resolve `gradient` into a CSS background. Accepts a tone name, a [from,to]
// pair (tone or color), or a raw CSS gradient/color string.
function toneToVar(t: string) {
	return /^(--|#|rgb|hsl|var\(|linear|radial|conic)/.test(t) ? t : `var(--bgl-${t})`
}
const gradientCss = computed(() => {
	const g = props.gradient
	if (!g) return undefined
	if (Array.isArray(g)) return `linear-gradient(150deg, ${toneToVar(g[0])}, ${toneToVar(g[1])})`
	if (/^(linear|radial|conic)-gradient/.test(g)) return g
	// single tone → subtle two-stop using the tone and a darker mix
	const c = toneToVar(g)
	return `linear-gradient(150deg, ${c}, color-mix(in srgb, ${c} 55%, #000))`
})

const ratioClass = computed(() => (props.ratio ? `ratio-${props.ratio}` : ''))
// When the frame has no intrinsic height (no ratio + no explicit height), the
// absolute-fill img would collapse the box to 0. In that case let the img flow
// naturally (relative) so the frame sizes to the image.
const frameHasHeight = computed(() => props.ratio !== undefined || props.height !== undefined)
const radiusStyle = computed(() => {
	if (props.rounded === undefined || props.rounded === false) return undefined
	return typeof props.rounded === 'number' ? `${props.rounded}px` : 'var(--bgl-card-radius, 12px)'
})
const imgFit = computed(() => props.fit ?? ((props.ratio || props.gradient) ? 'cover' : undefined))
</script>

<template>
	<!-- Framed mode: ratio box with gradient base, blended photo, and overlay slot. -->
	<div
		v-if="framed"
		class="bgl-image-frame relative overflow-hidden"
		:class="ratioClass"
		:style="{
			width: normalizeDimension(width),
			height: ratio ? undefined : normalizeDimension(height),
			background: gradientCss,
			borderRadius: radiusStyle,
		}"
	>
		<img
			v-if="imageSrc"
			:src="imageSrc"
			:alt="alt"
			class="bgl-image-frame-img"
			:class="frameHasHeight ? 'absolute-fill' : 'bgl-image-frame-img--flow'"
			:style="{ objectFit: imgFit, mixBlendMode: blend } as StyleValue"
		>
		<Skeleton v-else-if="!gradientCss && !loadingError" class="absolute-fill" />
		<div v-else-if="loadingError && !gradientCss" class="flex-center absolute-fill error-image">
			<Icon name="broken_image" />
		</div>
		<slot />
	</div>

	<figcaption v-else-if="caption">
		<img
			v-if="imageSrc"
			:src="imageSrc"
			v-bind="$attrs"
			:alt="alt"
			:width="normalizeDimension(width)"
			:height="normalizeDimension(height)"
		>
		<Skeleton
			v-else
			class="img-web-kit"
			:width="normalizeDimension(width)"
			:height="normalizeDimension(height)"
		/>
	</figcaption>

	<img
		v-else-if="imageSrc"
		:src="imageSrc"
		v-bind="$attrs"
		:alt="alt"
		:width="normalizeDimension(width)"
		:height="normalizeDimension(height)"
	>
	<div
		v-else-if="loadingError"
		class="flex-center error-image"
		:style="{
			width: normalizeDimension(width),
			height: normalizeDimension(height),
		}"
	>
		<Icon name="broken_image" />
	</div>
	<Skeleton
		v-else
		class="img-web-kit"
		:width="normalizeDimension(width)"
		:height="normalizeDimension(height)"
	/>
</template>

<style scoped>

.img-web-kit {
max-width: 100%;
vertical-align: middle;
border: 0;
width: 100%;
}
.error-image {
background-color: var(--bgl-skeleton-bg);
}

.bgl-image-frame { display: block; }
.bgl-image-frame-img { width: 100%; height: 100%; }
/* no ratio/height → flow naturally so the frame gets the image's height (avoids 0×0 collapse) */
.bgl-image-frame-img--flow { position: relative; height: auto; display: block; }

</style>
