---
import {
	getImage,
	imageConfig,
	inferRemoteSize,
	type LocalImageProps,
	type RemoteImageProps,
} from 'astro:assets';
import * as mime from 'mrmime';
import { isESMImportedImage, isRemoteImage, resolveSrc } from '../dist/assets/utils/imageKind.js';
import { AstroError, AstroErrorData } from '../dist/core/errors/index.js';
import type {
	GetImageResult,
	ImageOutputFormat,
	UnresolvedImageTransform,
} from '../dist/types/public/index.js';
import type { HTMLAttributes } from '../types.js';

export type Props = (LocalImageProps | RemoteImageProps) & {
	formats?: ImageOutputFormat[];
	fallbackFormat?: ImageOutputFormat;
	pictureAttributes?: HTMLAttributes<'picture'>;
};

const defaultFormats = ['webp'] as const;
const defaultFallbackFormat = 'png' as const;

// Certain formats don't want PNG fallbacks:
// - GIF will typically want to stay as a gif, either for animation or for the lower amount of colors
// - SVGs can't be converted to raster formats in most cases
// - JPEGs compress photographs and high-noise images better than PNG in most cases
// For those, we'll use the original format as the fallback instead.
const specialFormatsFallback = ['gif', 'svg', 'jpg', 'jpeg'] as const;

const { formats = defaultFormats, pictureAttributes = {}, fallbackFormat, ...props } = Astro.props;

if (props.alt === undefined || props.alt === null) {
	throw new AstroError(AstroErrorData.ImageMissingAlt);
}

// Picture attribute inherit scoped styles from class and attributes
const scopedStyleClass = props.class?.match(/\bastro-\w{8}\b/)?.[0];
if (scopedStyleClass) {
	if (pictureAttributes.class) {
		pictureAttributes.class = `${pictureAttributes.class} ${scopedStyleClass}`;
	} else {
		pictureAttributes.class = scopedStyleClass;
	}
}

const layout = props.layout ?? imageConfig.layout ?? 'none';
const useResponsive = layout !== 'none';

if (useResponsive) {
	props.layout ??= imageConfig.layout;
	props.fit ??= imageConfig.objectFit ?? 'cover';
	props.position ??= imageConfig.objectPosition ?? 'center';
} else if (imageConfig.objectFit || imageConfig.objectPosition) {
	props.fit ??= imageConfig.objectFit;
	props.position ??= imageConfig.objectPosition;
}

for (const key in props) {
	if (key.startsWith('data-astro-cid')) {
		// @ts-expect-error This is for island props so they're not properly typed
		pictureAttributes[key] = props[key];
	}
}

const originalSrc = await resolveSrc(props.src);

// When inferSize is set for a remote image, resolve dimensions once and thread them through
// to all getImage() calls. This avoids multiple redundant HTTP requests to the same URL,
// which can trigger rate-limiting (HTTP 429) on servers like Wikimedia.
if (props.inferSize && isRemoteImage(originalSrc)) {
	const remoteSize = await inferRemoteSize(originalSrc);
	delete props.inferSize;
	props.width ??= remoteSize.width;
	props.height ??= remoteSize.height;
}

const optimizedImages: GetImageResult[] = await Promise.all(
	formats.map(
		async (format) =>
			await getImage({
				...props,
				src: originalSrc,
				format: format,
				widths: props.widths,
				densities: props.densities,
			} as UnresolvedImageTransform),
	),
);

// Clone the `src` object if it's an ESM import so that we don't refer to any properties of the original object
// Causing our generate step to think the image is used outside of the image optimization pipeline
const clonedSrc = isESMImportedImage(originalSrc)
	? // @ts-expect-error - clone is a private, hidden prop
		(originalSrc.clone ?? originalSrc)
	: originalSrc;

let resultFallbackFormat = fallbackFormat ?? defaultFallbackFormat;
if (
	!fallbackFormat &&
	isESMImportedImage(clonedSrc) &&
	(specialFormatsFallback as ReadonlyArray<string>).includes(clonedSrc.format)
) {
	resultFallbackFormat = clonedSrc.format;
}

const fallbackImage = await getImage({
	...props,
	format: resultFallbackFormat,
	widths: props.widths,
	densities: props.densities,
} as UnresolvedImageTransform);

const imgAdditionalAttributes: HTMLAttributes<'img'> = {};
const sourceAdditionalAttributes: HTMLAttributes<'source'> = {};

// Propagate the `sizes` attribute to the `source` elements
if (props.sizes) {
	sourceAdditionalAttributes.sizes = props.sizes;
}

if (fallbackImage.srcSet.values.length > 0) {
	imgAdditionalAttributes.srcset = fallbackImage.srcSet.attribute;
}

if (import.meta.env.DEV) {
	imgAdditionalAttributes['data-image-component'] = 'true';
}

const { class: className, ...attributes } = {
	...imgAdditionalAttributes,
	...fallbackImage.attributes,
};
---

<picture {...pictureAttributes}>
	{
		Object.entries(optimizedImages).map(([_, image]) => {
			const srcsetAttribute =
				props.densities || (!props.densities && !props.widths && !useResponsive)
					? `${image.src}${image.srcSet.values.length > 0 ? ', ' + image.srcSet.attribute : ''}`
					: image.srcSet.attribute;
			return (
				<source
					srcset={srcsetAttribute}
					type={mime.lookup(image.options.format ?? image.src) ?? `image/${image.options.format}`}
					{...sourceAdditionalAttributes}
				/>
			);
		})
	}
	{/* Applying class outside of the spread prevents it from applying unnecessary astro-* classes */}
	<img src={fallbackImage.src} {...attributes} class={className} />
</picture>
