---
// @ts-ignore virtual module
import icons, { config } from "virtual:astro-icon";
// @ts-ignore generated by typegen
import type { Icon } from "virtual:astro-icon";
import { getIconData, iconToSVG, replaceIDs } from "@iconify/utils";
import type { HTMLAttributes } from "astro/types";
import { cache } from "./cache.js";
import type { IconifyIconBuildResult } from "@iconify/utils/lib/svg/build.js";

interface AccessibleElementProps {
  id?: string;
  value: string;
}

interface Props extends HTMLAttributes<"svg"> {
  name: Icon;
  "is:inline"?: boolean;
  title?: string | AccessibleElementProps;
  desc?: string | AccessibleElementProps;
  size?: number | string;
  width?: number | string;
  height?: number | string;
}

class AstroIconError extends Error {
  public hint: string = "";

  constructor(message: string) {
    super(message);
  }
}

const localsKey = Astro.locals;
const {
  name = "",
  title,
  desc,
  "is:inline": inline = false,
  ...props
} = Astro.props;

const normalizeAccessibleElement = (value?: string | AccessibleElementProps) =>
  typeof value === "string" ? { value } : value;
const normalizedTitle = normalizeAccessibleElement(title);
const normalizedDesc = normalizeAccessibleElement(desc);
const map = cache.get(localsKey) ?? new Map();
const i = map.get(name) ?? 0;
map.set(name, i + 1);
cache.set(localsKey, map);

const { include = {} } = config;
const sets = Object.keys(include);

const includeSymbol = !inline && i === 0;

let [setName, iconName] = (name as string).split(":");

if (!setName && iconName) {
  const err = new AstroIconError(`Invalid "name" provided!`);
  if (import.meta.env.DEV) {
    err.hint = `The provided value of "${name}" is invalid.\n\nDid you forget the icon set name? If you were attemping to reference a local icon, use the icon's name directly. (ie. "${iconName}")`;
  }
  throw err;
}

// No iconName, assume local icon reference
if (!iconName) {
  // Assign the provided setName to the iconName
  iconName = setName;
  setName = "local";

  // Check if the local icon set exists
  if (!icons[setName]) {
    const err = new AstroIconError('Unable to load the "local" icon set!');
    if (import.meta.env.DEV) {
      err.hint =
        'It looks like the "local" set was not loaded.\n\nDid you forget to create the icon directory or to update your config?';
    }
    throw err;
  }

  // Check if the icon is missing from the local collection
  if (!(iconName in icons[setName].icons)) {
    const err = new AstroIconError(`Unable to locate "${name}" icon!`);
    if (import.meta.env.DEV) {
      err.hint = `The icon named "${iconName}" was not found in your local icon directory.\n\nDid you forget to configure your icon directory or make a typo?`;
    }
    throw err;
  }
}

const collection = icons[setName];

// Iconify collection not configured correctly
if (!collection) {
  const err = new AstroIconError(`Unable to locate the "${setName}" icon set!`);
  if (import.meta.env.DEV) {
    if (sets.includes(setName)) {
      err.hint = `It looks like the "${setName}" set was not loaded.\n\nDid you install the "@iconify-json/${setName}" dependency?`;
    } else {
      err.hint = `It looks like the "${setName}" set is not included in your configuration.\n\nDo you need to add the "${setName}" set?`;
    }
  }
  throw err;
}

const iconData = getIconData(collection, iconName ?? setName);
// Missing icon from the icon collection
if (!iconData) {
  const err = new AstroIconError(`Unable to locate "${name}" icon!`);
  if (import.meta.env.DEV) {
    const [maybeStar] = include[setName];
    if (maybeStar === "*" || include[setName].includes(iconName)) {
      err.hint = `The "${setName}" set does not include an icon named "${iconName}".\n\nIs this a typo?`;
    } else {
      err.hint = `The "${setName}" set is not configured to include an icon named "${iconName}".\n\nDo you need to add it to your configuration?`;
    }
  }
  throw err;
}

const id = `ai:${collection.prefix}:${iconName ?? setName}`;

if (props.size) {
  props.width = props.size;
  props.height = props.size;
  delete props.size;
}
const renderData = iconToSVG(iconData);
const attributes = renderData.attributes as Partial<
  IconifyIconBuildResult["attributes"]
>;
const normalizedProps = { ...attributes, ...props };
const normalizedBody = replaceIDs(renderData.body);

const [minX, minY, vbWidth, vbHeight] =
  attributes.viewBox?.split(/[\s,]+/).map(Number) ?? [];
// Without this the artwork renders offset by its origin and clipped (https://github.com/natemoo-re/astro-icon/issues/212).
const hasOffsetOrigin =
  [minX, minY, vbWidth, vbHeight].every(Number.isFinite) &&
  (minX !== 0 || minY !== 0);
const symbolViewBox = hasOffsetOrigin ? attributes.viewBox : undefined;
const useRect = hasOffsetOrigin
  ? { x: minX, y: minY, width: vbWidth, height: vbHeight }
  : {};
---

<svg {...normalizedProps} data-icon={name}>
  {
    normalizedTitle && (
      <title id={normalizedTitle.id}>{normalizedTitle.value}</title>
    )
  }
  {normalizedDesc && <desc id={normalizedDesc.id}>{normalizedDesc.value}</desc>}
  {
    inline ? (
      <Fragment id={id} set:html={normalizedBody} />
    ) : (
      <Fragment>
        {includeSymbol && (
          <symbol id={id} viewBox={symbolViewBox} set:html={normalizedBody} />
        )}
        <use href={`#${id}`} {...useRect} />
      </Fragment>
    )
  }
</svg>
