// Copyright © onWidget <https://github.com/onwidget>
// SPDX-License-Identifier: MIT
import { getImage } from "astro:assets";
import { transformUrl, parseUrl } from "unpic";

import type { ImageMetadata } from "astro";
import type { HTMLAttributes } from "astro/types";

type Layout =
    | "fixed"
    | "constrained"
    | "fullWidth"
    | "cover"
    | "responsive"
    | "contained";

export interface ImageProps extends Omit<HTMLAttributes<"img">, "src"> {
    src?: string | ImageMetadata | null;
    width?: string | number | null;
    height?: string | number | null;
    alt?: string | null;
    loading?: "eager" | "lazy" | null;
    decoding?: "sync" | "async" | "auto" | null;
    style?: string;
    srcset?: string | null;
    sizes?: string | null;
    fetchpriority?: "high" | "low" | "auto" | null;

    layout?: Layout;
    widths?: number[] | null;
    aspectRatio?: string | number | null;
    objectPosition?: string;

    format?: string;
}

export type ImagesOptimizer = (
    image: ImageMetadata | string,
    breakpoints: number[],
    width?: number,
    height?: number,
    format?: string,
) => Promise<Array<{ src: string; width: number }>>;

/* ******* */
const config = {
    // FIXME: Use this when image.width is minor than deviceSizes
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],

    deviceSizes: [
        640, // older and lower-end phones
        750, // iPhone 6-8
        828, // iPhone XR/11
        960, // older horizontal phones
        1080, // iPhone 6-8 Plus
        1280, // 720p
        1668, // Various iPads
        1920, // 1080p
        2048, // QXGA
        2560, // WQXGA
        3200, // QHD+
        3840, // 4K
        4480, // 4.5K
        5120, // 5K
        6016, // 6K
    ],

    formats: ["image/webp"],
};

const computeHeight = (width: number, aspectRatio: number) => {
    return Math.floor(width / aspectRatio);
};

const parseAspectRatio = (
    aspectRatio: number | string | null | undefined,
): number | undefined => {
    if (typeof aspectRatio === "number") {
        return aspectRatio;
    }

    if (typeof aspectRatio === "string") {
        const match = aspectRatio.match(/(\d+)\s*[/:]\s*(\d+)/);

        if (match) {
            const [, num, den] = match.map(Number);
            if (den && !Number.isNaN(num)) {
                return num / den;
            }
        } else {
            const numericValue = parseFloat(aspectRatio);
            if (!Number.isNaN(numericValue)) {
                return numericValue;
            }
        }
    }

    return undefined;
};

/**
 * Gets the `sizes` attribute for an image, based on the layout and width
 */
export const getSizes = (
    width?: number,
    layout?: Layout,
): string | undefined => {
    if (!width || !layout) {
        return undefined;
    }
    switch (layout) {
        // If screen is wider than the max size, image width is the max size,
        // otherwise it's the width of the screen
        case `constrained`:
            return `(min-width: ${width}px) ${width}px, 100vw`;

        // Image is always the same width, whatever the size of the screen
        case `fixed`:
            return `${width}px`;

        // Image is always the width of the screen
        case `fullWidth`:
            return `100vw`;

        default:
            return undefined;
    }
};

const pixelate = (value?: number) =>
    value || value === 0 ? `${value}px` : undefined;

const getStyle = ({
    width,
    height,
    aspectRatio,
    layout,
    objectFit = "cover",
    objectPosition = "center",
    background,
}: {
    width?: number;
    height?: number;
    aspectRatio?: number;
    objectFit?: string;
    objectPosition?: string;
    layout?: string;
    background?: string;
}) => {
    const styleEntries: Array<[prop: string, value: string | undefined]> = [
        ["object-fit", objectFit],
        ["object-position", objectPosition],
    ];

    // If background is a URL, set it to cover the image and not repeat
    if (
        background?.startsWith("https:") ||
        background?.startsWith("http:") ||
        background?.startsWith("data:")
    ) {
        styleEntries.push(["background-image", `url(${background})`]);
        styleEntries.push(["background-size", "cover"]);
        styleEntries.push(["background-repeat", "no-repeat"]);
    } else {
        styleEntries.push(["background", background]);
    }
    if (layout === "fixed") {
        styleEntries.push(["width", pixelate(width)]);
        styleEntries.push(["height", pixelate(height)]);
        styleEntries.push(["object-position", "top left"]);
    }
    if (layout === "constrained") {
        styleEntries.push(["max-width", pixelate(width)]);
        styleEntries.push(["max-height", pixelate(height)]);
        styleEntries.push([
            "aspect-ratio",
            aspectRatio ? `${aspectRatio}` : undefined,
        ]);
        styleEntries.push(["width", "100%"]);
    }
    if (layout === "fullWidth") {
        styleEntries.push(["width", "100%"]);
        styleEntries.push([
            "aspect-ratio",
            aspectRatio ? `${aspectRatio}` : undefined,
        ]);
        styleEntries.push(["height", pixelate(height)]);
    }
    if (layout === "responsive") {
        styleEntries.push(["width", "100%"]);
        styleEntries.push(["height", "auto"]);
        styleEntries.push([
            "aspect-ratio",
            aspectRatio ? `${aspectRatio}` : undefined,
        ]);
    }
    if (layout === "contained") {
        styleEntries.push(["max-width", "100%"]);
        styleEntries.push(["max-height", "100%"]);
        styleEntries.push(["object-fit", "contain"]);
        styleEntries.push([
            "aspect-ratio",
            aspectRatio ? `${aspectRatio}` : undefined,
        ]);
    }
    if (layout === "cover") {
        styleEntries.push(["max-width", "100%"]);
        styleEntries.push(["max-height", "100%"]);
    }

    const styles = Object.fromEntries(
        styleEntries.filter(([, value]) => value),
    );

    return Object.entries(styles)
        .map(([key, value]) => `${key}: ${value};`)
        .join(" ");
};

const getBreakpoints = ({
    width,
    breakpoints,
    layout,
}: {
    width?: number;
    breakpoints?: number[];
    layout: Layout;
}): number[] => {
    if (
        layout === "fullWidth" ||
        layout === "cover" ||
        layout === "responsive" ||
        layout === "contained"
    ) {
        return breakpoints || config.deviceSizes;
    }
    if (!width) {
        return [];
    }
    const doubleWidth = width * 2;
    if (layout === "fixed") {
        return [width, doubleWidth];
    }
    if (layout === "constrained") {
        return [
            // Always include the image at 1x and 2x the specified width
            width,
            doubleWidth,
            // Filter out any resolutions that are larger than the double-res image
            ...(breakpoints || config.deviceSizes).filter(
                (w) => w < doubleWidth,
            ),
        ];
    }

    return [];
};

/* ** */
export const astroAssetsOptimizer: ImagesOptimizer = (
    image,
    breakpoints,
    _width,
    _height,
    format = undefined,
) => {
    if (!image) {
        return Promise.resolve([]);
    }

    return Promise.all(
        breakpoints.map(async (w: number) => {
            const result = await getImage({
                src: image,
                width: w,
                inferSize: true,
                ...(format ? { format: format } : {}),
            });

            return {
                src: result?.src,
                width: result?.attributes?.width ?? w,
                height: result?.attributes?.height,
            };
        }),
    );
};

export const isUnpicCompatible = (image: string) => {
    return typeof parseUrl(image) !== "undefined";
};

/* ** */
export const unpicOptimizer: ImagesOptimizer = (
    image,
    breakpoints,
    width,
    height,
    format = undefined,
) => {
    if (!image || typeof image !== "string") {
        return Promise.resolve([]);
    }

    const urlParsed = parseUrl(image);
    if (!urlParsed) {
        return Promise.resolve([]);
    }

    return Promise.all(
        breakpoints.map((w: number) => {
            const _height =
                width && height ? computeHeight(w, width / height) : height;
            const url =
                transformUrl({
                    url: image,
                    width: w,
                    height: _height,
                    cdn: urlParsed.cdn,
                    ...(format ? { format: format } : {}),
                }) || image;
            return {
                src: String(url),
                width: w,
                height: _height,
            };
        }),
    );
};

/* ** */
export async function getImagesOptimized(
    image: ImageMetadata | string,
    {
        src: _,
        width,
        height,
        sizes,
        aspectRatio,
        objectPosition,
        widths,
        layout = "constrained",
        style = "",
        format,
        ...rest
    }: ImageProps,
    transform: ImagesOptimizer = () => Promise.resolve([]),
): Promise<{ src: string; attributes: HTMLAttributes<"img"> }> {
    if (typeof image !== "string") {
        width ||= Number(image.width) || undefined;
        height ||=
            typeof width === "number"
                ? computeHeight(width, image.width / image.height)
                : undefined;
    }

    width = (width && Number(width)) || undefined;
    height = (height && Number(height)) || undefined;

    widths ||= config.deviceSizes;
    sizes ||= getSizes(Number(width) || undefined, layout);
    aspectRatio = parseAspectRatio(aspectRatio);

    // Calculate dimensions from aspect ratio
    if (aspectRatio) {
        if (width) {
            if (height) {
                /* empty */
            } else {
                height = width / aspectRatio;
            }
        } else if (height) {
            width = Number(height * aspectRatio);
        } else if (layout !== "fullWidth") {
            // Fullwidth images have 100% width, so aspectRatio is applicable
            console.error(
                "When aspectRatio is set, either width or height must also be set",
            );
            console.error("Image", image);
        }
    } else if (width && height) {
        aspectRatio = width / height;
    } else if (layout !== "fullWidth") {
        // Fullwidth images don't need dimensions
        console.error(
            "Either aspectRatio or both width and height must be set",
        );
        console.error("Image", image);
    }

    let breakpoints = getBreakpoints({
        width: width,
        breakpoints: widths,
        layout: layout,
    });
    breakpoints = [...new Set(breakpoints)].sort((a, b) => a - b);

    const srcset = (
        await transform(
            image,
            breakpoints,
            Number(width) || undefined,
            Number(height) || undefined,
            format,
        )
    )
        .map(({ src, width }) => `${src} ${width}w`)
        .join(", ");

    return {
        src: typeof image === "string" ? image : image.src,
        attributes: {
            width: width,
            height: height,
            srcset: srcset || undefined,
            sizes: sizes,
            style: `${getStyle({
                width: width,
                height: height,
                aspectRatio: aspectRatio,
                objectPosition: objectPosition,
                layout: layout,
            })}${style ?? ""}`,
            ...rest,
        },
    };
}
