<script lang="ts">
  /**
   * Smart CMS-Image-Component.
   * - Automatisches AVIF→WebP→JPEG `<picture>`.
   * - Retina-srcset per `densities` (Default [1, 2]).
   * - CLS=0 via `width`+`height` (oder aus `aspect` abgeleitet).
   * - Focal durchgereicht (cover-Crop am Motiv).
   *
   * `src` = rohe CMS-URL (ohne /cms-images-Wrapper). Die Component baut
   * die transformierten /cms-images?…-URLs selbst.
   */
  import { getCmsImageUrl, type RustyImageTransformParams } from "$lib/rusty-image";

  interface Props {
    /** Rohe CMS-URL (z.B. https://cms.pm86.de/api/assets/...). */
    src: string;
    /** Ziel-Renderbreite in CSS-px (für Attribut + srcset-Basis). */
    width: number;
    /** Ziel-Renderhöhe; alternativ `aspect`. Mindestens eins muss gegeben sein. */
    height?: number;
    /** Seitenverhältnis, z.B. "16/9", "3/2". Alternativ zu `height`. */
    aspect?: string;
    /** Focal-Point aus CMS-Image-Field. */
    focal?: { x: number; y: number } | null;
    alt?: string;
    /** `sizes`-Attribut für Media-Queries; default `${width}px`. */
    sizes?: string;
    loading?: "lazy" | "eager";
    fetchpriority?: "high" | "auto" | "low";
    decoding?: "async" | "sync" | "auto";
    class?: string;
    style?: string;
    /** Pixel-Dichten fürs srcset. Default [1, 2] (Retina). */
    densities?: number[];
    /** Format-Fallback-Kette. Default ['webp','jpeg']. AVIF bewusst ausgelassen — server-seitiger Encoder ist 5-10× langsamer als WebP, Bandbreitengewinn marginal. */
    formats?: ("avif" | "webp" | "jpeg")[];
    /** JPEG/WebP-Qualität. Default 75. */
    quality?: number;
    fit?: "cover" | "contain" | "fill";
    /** Cache-Bust-Tag (z.B. post._updated) → `_v`-Param, bricht Browser-HTTP-Cache. */
    version?: string;
  }

  let {
    src,
    width,
    height,
    aspect,
    focal = null,
    alt = "",
    sizes,
    loading = "lazy",
    fetchpriority,
    decoding = "async",
    class: className = "",
    style,
    densities = [1, 2],
    formats = ["webp", "jpeg"],
    quality = 75,
    fit = "cover",
    version,
  }: Props = $props();

  function parseAr(s: string | undefined): number | null {
    if (!s) return null;
    const m = s.match(/^(\d+(?:\.\d+)?)\s*[/:x]\s*(\d+(?:\.\d+)?)$/);
    if (!m) return null;
    const w = Number(m[1]);
    const h = Number(m[2]);
    if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) return null;
    return w / h;
  }

  // Derive aspect from either the explicit `aspect` prop or the width/height pair.
  // Both `arString` (URL `ar=` param) and `arValue` (height-derivation) must read
  // from the SAME source — otherwise the URL requests a different shape than the
  // `<img>` reserves and the image flickers when it loads.
  const arString = $derived(
    aspect ??
      (height && width ? `${width}/${height}` : undefined),
  );
  const arValue = $derived(parseAr(arString));
  const computedHeight = $derived(
    height ?? (arValue ? Math.round(width / arValue) : undefined),
  );
  const sizesAttr = $derived(sizes ?? `${width}px`);

  function baseParams(format: "avif" | "webp" | "jpeg", w: number): RustyImageTransformParams {
    return {
      width: w,
      format,
      fit,
      quality,
      ...(arString ? { ar: arString } : {}),
      ...(focal ? { focalX: focal.x, focalY: focal.y } : {}),
      ...(arString && fit !== "contain"
        ? { height: Math.round(w / (arValue ?? 1)) }
        : {}),
      ...(version ? { version } : {}),
    };
  }

  function buildSrcset(format: "avif" | "webp" | "jpeg"): string {
    return densities
      .map((d) => {
        const w = Math.round(width * d);
        return `${getCmsImageUrl(src, baseParams(format, w))} ${d}x`;
      })
      .join(", ");
  }

  const fallbackFormat = $derived<"avif" | "webp" | "jpeg">(
    formats.includes("jpeg") ? "jpeg" : formats[formats.length - 1] ?? "jpeg",
  );
  const fallbackUrl = $derived(
    getCmsImageUrl(src, baseParams(fallbackFormat, width)),
  );
  const fallbackSrcset = $derived(buildSrcset(fallbackFormat));

  const pictureSources = $derived(
    formats.filter((f) => f !== fallbackFormat).map((f) => ({
      type: `image/${f}`,
      srcset: buildSrcset(f),
    })),
  );
</script>

<picture>
  {#each pictureSources as s (s.type)}
    <source type={s.type} srcset={s.srcset} sizes={sizesAttr} />
  {/each}
  <img
    src={fallbackUrl}
    srcset={fallbackSrcset}
    sizes={sizesAttr}
    {alt}
    width={width}
    height={computedHeight}
    class={className}
    {style}
    {loading}
    {decoding}
    {fetchpriority}
  />
</picture>
